@livelike/react-native
Version:
LiveLike React Native package
467 lines (438 loc) • 12.9 kB
text/typescript
import {
CHOICE_WIDGET_KIND,
IRewardTransaction,
IWidgetChoiceItem,
IWidgetEarnableReward,
IWidgetInteraction,
IWidgetOptionItem,
IWidgetPayload,
MULTI_INTERACTION_WIDGET_KINDS,
OPTION_WIDGET_KIND,
SINGLE_INTERACTION_WIDGET_KINDS,
WidgetKind,
} from '@livelike/javascript';
import { WidgetResultState, WidgetUIPhase } from '../types';
import { createStore } from './store';
export type WidgetState = {
widgetPayload: IWidgetPayload;
widgetUIPhase?: WidgetUIPhase;
widgetInteractions?: IWidgetInteraction[];
selectedOptionIndex?: number;
widgetResultState?: WidgetResultState;
widgetRewards?: IWidgetEarnableReward[];
isTimelineWidget?: boolean;
};
export type WidgetStoreValue = Record<string, WidgetState>;
const initialWidgetStoreValue: WidgetStoreValue = {};
export const widgetStore = createStore(initialWidgetStoreValue);
export type BaseWidgetActionArgs = {
widgetId: string;
};
export type UpdateWidgetPhaseActionArgs = BaseWidgetActionArgs & {
widgetUIPhase: WidgetUIPhase;
};
export type UpdateWidgetChoicesActionArgs = BaseWidgetActionArgs & {
widgetChoices: Pick<IWidgetChoiceItem, 'id' | 'answer_count'>[];
};
export type UpdateWidgetOptionsActionArgs = BaseWidgetActionArgs & {
widgetOptions: Pick<IWidgetOptionItem, 'id' | 'vote_count'>[];
};
export type UpdateWidgetInteractionActionArgs = BaseWidgetActionArgs & {
widgetInteractions: IWidgetInteraction[];
};
export type UpdateSelectedOptionIndexActionArgs = BaseWidgetActionArgs & {
selectedOptionIndex: number;
};
export type UpdateWidgetResultStateActionArgs = BaseWidgetActionArgs & {
widgetResultState: WidgetResultState;
};
export type UpdateWidgetRewardsActionArgs = BaseWidgetActionArgs & {
widgetRewards: IWidgetEarnableReward[];
};
export type UpdateWidgetStateActionArgs = BaseWidgetActionArgs & {
widgetState: Partial<WidgetState>;
};
export type UpdateWidgetAverageMagnitudeArgs = BaseWidgetActionArgs & {
averageMagnitude: string;
};
export const widgetStoreActions = {
updateWidgetStateAction({
widgetId,
widgetState,
}: UpdateWidgetStateActionArgs) {
const prevWidgetState = widgetStore.get()[widgetId];
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...prevWidgetState,
...widgetState,
},
});
},
updateWidgetChoicesAction({
widgetId,
widgetChoices,
}: UpdateWidgetChoicesActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget choices, widget not found for widgetId=${widgetId}`
);
}
// check if there's any choice change
const changedChoices = widgetState.widgetPayload.choices.filter(
(choice, index) => {
return Object.entries(widgetChoices[index]).some(
([key, value]) => choice[key] !== widgetChoices[key]
);
}
);
if (!changedChoices.length) {
return;
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetPayload: {
...widgetState.widgetPayload,
choices: widgetChoices.map((choice, index) => ({
...widgetState.widgetPayload?.choices?.[index],
...choice,
})),
},
},
});
},
updateWidgetOptionsAction({
widgetId,
widgetOptions,
}: UpdateWidgetOptionsActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget options, widget not found for widgetId=${widgetId}`
);
}
// check if there's any option vote change
const changedOptions = widgetState.widgetPayload.options.filter(
(option, index) => {
return Object.entries(widgetOptions[index]).some(
([key, value]) => option[key] !== widgetOptions[index][key]
);
}
);
if (!changedOptions.length) {
return;
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetPayload: {
...widgetState.widgetPayload,
options: widgetOptions.map((option, index) => ({
...widgetState.widgetPayload?.options?.[index],
...option,
})),
},
},
});
},
updateWidgetUIPhaseAction({
widgetId,
widgetUIPhase,
}: UpdateWidgetPhaseActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error('Error while updating widget phase, Widget Id not found');
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetUIPhase,
},
});
},
updateWidgetInteractionAction({
widgetId,
widgetInteractions,
}: UpdateWidgetInteractionActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget interaction, widget state not found for widgetId=${widgetId}`
);
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetInteractions,
widgetResultState: getWidgetResultState({
...widgetState,
widgetInteractions,
}),
widgetUIPhase: getWidgetUIPhaseOnInteractionUpdate(widgetState),
},
});
},
updateSelectedOptionIndexAction({
widgetId,
selectedOptionIndex,
}: UpdateSelectedOptionIndexActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget selected option, widget state not found for widgetId=${widgetId}`
);
}
const { widgetPayload, selectedOptionIndex: prevSelectedOptionIndex } =
widgetState;
const { options, choices, kind } = widgetPayload;
const isChoiceWidgetKind = CHOICE_WIDGET_KIND.includes(kind);
const updatedChoicesOrOptions = isChoiceWidgetKind
? getUpdatedChoices({
choices,
selectedOptionIndex,
prevSelectedOptionIndex,
})
: getUpdatedOptions({
options,
selectedOptionIndex,
prevSelectedOptionIndex,
});
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetPayload: {
...widgetState.widgetPayload,
[isChoiceWidgetKind ? 'choices' : 'options']: updatedChoicesOrOptions,
},
selectedOptionIndex,
},
});
},
updateWidgetResultStateAction({
widgetId,
widgetResultState,
}: UpdateWidgetResultStateActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget selected option, widget state not found for widgetId=${widgetId}`
);
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetResultState,
},
});
},
updateWidgetRewardsAction({
widgetId,
widgetRewards,
}: UpdateWidgetRewardsActionArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget selected option, widget state not found for widgetId=${widgetId}`
);
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetRewards,
},
});
},
updateWidgetAverageMagnitude({
widgetId,
averageMagnitude,
}: UpdateWidgetAverageMagnitudeArgs) {
const widgetState = widgetStore.get()[widgetId];
if (!widgetState) {
throw new Error(
`Error while updating widget selected option, widget state not found for widgetId=${widgetId}`
);
}
widgetStore.set({
...widgetStore.get(),
[widgetId]: {
...widgetState,
widgetPayload: {
...widgetState.widgetPayload,
average_magnitude: averageMagnitude,
},
},
});
},
};
function getUpdatedOptions({
options,
prevSelectedOptionIndex,
selectedOptionIndex,
}: {
options: IWidgetOptionItem[];
prevSelectedOptionIndex: number;
selectedOptionIndex: number;
}) {
let validOptionUpdate = true;
const updatedOptions = options.map((option, index) => {
let { vote_count } = option;
if (index === selectedOptionIndex) {
vote_count += 1;
} else if (index === prevSelectedOptionIndex) {
vote_count -= 1;
}
if (vote_count < 0) {
validOptionUpdate = false;
}
return {
...option,
vote_count,
};
});
return validOptionUpdate ? updatedOptions : options;
}
function getUpdatedChoices({
choices,
prevSelectedOptionIndex,
selectedOptionIndex,
}: {
choices: IWidgetChoiceItem[];
prevSelectedOptionIndex: number;
selectedOptionIndex: number;
}) {
return choices.map((choice, index) => {
let { answer_count } = choice;
if (index === selectedOptionIndex) {
answer_count += 1;
} else if (index === prevSelectedOptionIndex) {
answer_count -= 1;
}
return {
...choice,
answer_count,
};
});
}
export function getWidgetResultState(widgetState: WidgetState) {
const { widgetPayload, widgetInteractions } = widgetState;
switch (widgetPayload.kind) {
case WidgetKind.TEXT_PREDICTION:
case WidgetKind.IMAGE_PREDICTION:
case WidgetKind.TEXT_NUMBER_PREDICTION:
case WidgetKind.IMAGE_NUMBER_PREDICTION: {
const followUp =
widgetPayload?.follow_ups?.[widgetPayload?.follow_ups?.length - 1];
return widgetInteractions?.length &&
followUp &&
followUp.status === 'published'
? WidgetResultState.SHOWN
: WidgetResultState.HIDDEN;
}
case WidgetKind.TEXT_PREDICTION_FOLLOW_UP:
case WidgetKind.IMAGE_PREDICTION_FOLLOW_UP:
case WidgetKind.IMAGE_NUMBER_PREDICTION_FOLLOW_UP:
case WidgetKind.TEXT_NUMBER_PREDICTION_FOLLOW_UP: {
return WidgetResultState.SHOWN;
}
default: {
return widgetInteractions?.length
? WidgetResultState.SHOWN
: WidgetResultState.HIDDEN;
}
}
}
export function getWidgetUIPhaseOnInteractionUpdate(widgetState: WidgetState) {
const { widgetPayload } = widgetState;
if (SINGLE_INTERACTION_WIDGET_KINDS.includes(widgetPayload.kind)) {
return WidgetUIPhase.SUBMITTED;
} else if (MULTI_INTERACTION_WIDGET_KINDS.includes(widgetPayload.kind)) {
return WidgetUIPhase.INTERACTIVE;
}
return undefined;
}
export function getWidgeUIPhase(widgetState: WidgetState) {
const { widgetPayload, widgetInteractions } = widgetState;
if (
widgetPayload?.interactive_until &&
isTimeExpired(widgetPayload?.interactive_until)
) {
return WidgetUIPhase.EXPIRED;
}
const followUp =
widgetPayload?.follow_ups?.[widgetPayload?.follow_ups?.length - 1];
switch (widgetPayload.kind) {
case WidgetKind.TEXT_PREDICTION:
case WidgetKind.IMAGE_PREDICTION: {
return followUp && followUp.status === 'published'
? WidgetUIPhase.FOLLOW_UP_PUBLISHED
: WidgetUIPhase.INTERACTIVE;
}
case WidgetKind.TEXT_NUMBER_PREDICTION:
case WidgetKind.IMAGE_NUMBER_PREDICTION: {
if (followUp && followUp.status === 'published') {
return WidgetUIPhase.FOLLOW_UP_PUBLISHED;
}
return widgetInteractions?.length
? WidgetUIPhase.SUBMITTED
: WidgetUIPhase.INTERACTIVE;
}
case WidgetKind.TEXT_PREDICTION_FOLLOW_UP:
case WidgetKind.IMAGE_PREDICTION_FOLLOW_UP:
case WidgetKind.IMAGE_NUMBER_PREDICTION_FOLLOW_UP:
case WidgetKind.TEXT_NUMBER_PREDICTION_FOLLOW_UP: {
return WidgetUIPhase.FOLLOW_UP_PUBLISHED;
}
default: {
return widgetInteractions?.length
? WidgetUIPhase.SUBMITTED
: WidgetUIPhase.INTERACTIVE;
}
}
}
export function getSelectedOptionIndex({
widgetPayload,
widgetInteractions,
}: WidgetState) {
const { kind, choices, options } = widgetPayload;
if (CHOICE_WIDGET_KIND.includes(kind)) {
return choices.findIndex(({ id }) =>
widgetInteractions?.find(({ choice_id }) => choice_id === id)
);
} else if (OPTION_WIDGET_KIND.includes(kind)) {
return options.findIndex(({ id }) =>
widgetInteractions?.find(({ option_id }) => option_id === id)
);
}
return -1;
}
export function getWidgetRewardsFromRewardTransactions(
rewardTransactions: IRewardTransaction[]
): IWidgetEarnableReward[] {
return rewardTransactions.map(
({
reward_item_amount,
reward_item_name,
reward_item_id,
reward_action_key,
}) => ({
reward_item_amount,
reward_item_name,
reward_item_id,
reward_action_key,
})
);
}
export function isTimeExpired(expiryTimestamp: string) {
const expiredTime = new Date(expiryTimestamp);
const timeout = expiredTime.getTime() - Date.now();
return timeout <= 0;
}