Skip to content

Commit

Permalink
Reduxify TBui.textFeedback (#884)
Browse files Browse the repository at this point in the history
  • Loading branch information
eritbh authored Jan 25, 2024
1 parent 40745b2 commit da081a1
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 64 deletions.
14 changes: 11 additions & 3 deletions extension/data/AppRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import {Provider} from 'react-redux';

import store from './store';

import {PageNotificationContainer} from './components/PageNotificationContainer';
import {TextFeedbackContainer} from './components/TextFeedbackContainer';

export default function () {
return (
<div className='tb-app-root'>
<PageNotificationContainer />
</div>
<Provider store={store}>
<div className='tb-app-root'>
<PageNotificationContainer />
<TextFeedbackContainer />
</div>
</Provider>
);
}
31 changes: 31 additions & 0 deletions extension/data/components/TextFeedbackContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {useSelector} from 'react-redux';
import {RootState} from '../store';
import {TextFeedbackLocation} from '../store/textFeedbackSlice';

export function TextFeedbackContainer () {
const currentMessage = useSelector((state: RootState) => state.textFeedback.current);

// TODO: dont judge me for this im just duplicating how it was done before.
// eventually we will have a way to do scoped component styles and this will
// be better
const style = currentMessage && currentMessage.location === TextFeedbackLocation.BOTTOM
? {
left: '5px',
bottom: '40px',
top: 'auto',
position: 'fixed',
} as const
: {
transform: 'translate(-50%)',
} as const;

return (
<>
{currentMessage && (
<div id='tb-feedback-window' className={currentMessage.kind} style={style}>
<span className='tb-feedback-text'>{currentMessage.message}</span>
</div>
)}
</>
);
}
21 changes: 21 additions & 0 deletions extension/data/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {combineReducers, configureStore, ThunkAction, UnknownAction} from '@reduxjs/toolkit';
import textFeedbackReducer from './textFeedbackSlice';

const rootReducer = combineReducers({
textFeedback: textFeedbackReducer,
});

const store = configureStore({
reducer: rootReducer,
});
export default store;

// TS concerns - see https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
UnknownAction
>;
61 changes: 61 additions & 0 deletions extension/data/store/textFeedbackSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {createSlice, type PayloadAction} from '@reduxjs/toolkit';
import {type AppThunk} from '.';

// TODO: remove `TBui.FEEDBACK_*` constants in favor of this enum
export enum TextFeedbackKind {
NEUTRAL = 'neutral',
POSITIVE = 'positive',
NEGATIVE = 'negative',
}

// TODO: remove `TBui.DISPLAY_*` constants in favor of this enum
export enum TextFeedbackLocation {
CENTER = 'center',
BOTTOM = 'bottom',
}

/** A text feedback message to be displayed. */
export interface TextFeedback {
message: string;
kind: TextFeedbackKind;
location: TextFeedbackLocation;
}

// alright time for redux shit

interface TextFeedbackState {
current: TextFeedback | null;
}
export const textFeedbackSlice = createSlice({
name: 'textFeedback',
initialState: {
current: null,
} as TextFeedbackState,
reducers: {
set (state, action: PayloadAction<TextFeedback>) {
state.current = action.payload;
},
clear (state) {
state.current = null;
},
},
});
export default textFeedbackSlice.reducer;
export const {set, clear} = textFeedbackSlice.actions;

let removeTextFeedbackTimeout: number | null = null;
export const showTextFeedback = (message: TextFeedback, duration = 3000): AppThunk => dispatch => {
// cancel any pending removal from previous messages
if (removeTextFeedbackTimeout) {
clearTimeout(removeTextFeedbackTimeout);
}

// display the message
dispatch(set(message));

// queue the message to be removed after the duration
removeTextFeedbackTimeout = window.setTimeout(() => {
dispatch(clear());
removeTextFeedbackTimeout = null;
}, duration);
};
54 changes: 7 additions & 47 deletions extension/data/tbui.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import * as TBStorage from './tbstorage.js';
import {onDOMAttach} from './util/dom.ts';
import {reactRenderer} from './util/ui_interop.tsx';

import {showTextFeedback} from './store/textFeedbackSlice.ts';

import store from './store/index.ts';
import {icons} from './tbconstants.ts';
export {icons};

Expand Down Expand Up @@ -562,53 +565,10 @@ export function mapInput (labels, items) {
}

export function textFeedback (feedbackText, feedbackKind, displayDuration, displayLocation) {
if (!displayLocation) {
displayLocation = DISPLAY_CENTER;
}

// Without text we can't give feedback, the feedbackKind is required to avoid problems in the future.
if (feedbackText && feedbackKind) {
// If there is still a previous feedback element on the page we remove it.
$body.find('#tb-feedback-window').remove();

// build up the html, not that the class used is directly passed from the function allowing for easy addition of other kinds.
const feedbackElement = TBStorage.purify(
`<div id="tb-feedback-window" class="${feedbackKind}"><span class="tb-feedback-text">${feedbackText}</span></div>`,
);

// Add the element to the page.
$body.append(feedbackElement);

// center it nicely, yes this needs to be done like this if you want to make sure it is in the middle of the page where the user is currently looking.
const $feedbackWindow = $body.find('#tb-feedback-window');

switch (displayLocation) {
case DISPLAY_CENTER:
{
const feedbackLeftMargin = $feedbackWindow.outerWidth() / 2;
const feedbackTopMargin = $feedbackWindow.outerHeight() / 2;

$feedbackWindow.css({
'margin-left': `-${feedbackLeftMargin}px`,
'margin-top': `-${feedbackTopMargin}px`,
});
}
break;
case DISPLAY_BOTTOM:
{
$feedbackWindow.css({
left: '5px',
bottom: '40px',
top: 'auto',
position: 'fixed',
});
}
break;
}

// And fade out nicely after 3 seconds.
$feedbackWindow.delay(displayDuration ? displayDuration : 3000).fadeOut();
}
store.dispatch(showTextFeedback(
{message: feedbackText, kind: feedbackKind, location: displayLocation || DISPLAY_CENTER},
displayDuration || 3000,
));
}

// Our awesome long load spinner that ended up not being a spinner at all. It will attend the user to ongoing background operations with a warning when leaving the page.
Expand Down
Loading

0 comments on commit da081a1

Please sign in to comment.