Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/app-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ export { SseService } from 'services/server-sent-events';
// WIDGETS
export { WidgetSource, WidgetsService } from './services/widgets';
export { StreamBossService } from 'services/widgets/settings/stream-boss';
export { CreditsService } from 'services/widgets/settings/credits';
export { TipJarService } from 'services/widgets/settings/tip-jar';
export { MediaShareService } from 'services/widgets/settings/media-share';
export { AlertBoxService } from 'services/widgets/settings/alert-box';
Expand Down
6 changes: 3 additions & 3 deletions app/components-react/shared/inputs/FormFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
import * as inputs from './inputList';
import { TInputType, TSlobsInputProps } from './inputs';
import Form, { useForm } from './Form';
import { TInputMetadata } from './metadata';
import { IBaseMetadata } from './metadata';
import { ButtonGroup } from 'components-react/shared/ButtonGroup';
import { $t } from 'services/i18n';

Expand Down Expand Up @@ -36,7 +36,7 @@ const componentTable: {
};

interface IFormMetadata {
[value: string]: TInputMetadata;
[value: string]: IBaseMetadata;
}

export default function FormFactory(p: {
Expand Down Expand Up @@ -93,7 +93,7 @@ export default function FormFactory(p: {

function FormInput(p: {
id: string;
metadata: TInputMetadata<unknown>;
metadata: IBaseMetadata;
values: Dictionary<TInputValue>;
onChange: (key: string) => (value: TInputValue) => void;
}) {
Expand Down
18 changes: 4 additions & 14 deletions app/components-react/shared/inputs/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Rule } from 'antd/lib/form';
import { TInputValue } from './FormFactory';
import { IListOption } from './ListInput';
import { TInputType } from './inputs';

/**
* Metadata generator for inputs
* Provides some presets and helps with typechecking
*/
export const metadata = {
any: (options: IAnyMetadata) => options,
any: (options: IAnyMetadata & Record<string, any>) => options,
text: (options: ITextMetadata) => ({ ...options, type: 'text' }),
textarea: (options: ITextMetadata) => ({ ...options, type: 'textarea' }),
number: (options: INumberMetadata) => ({ ...options, type: 'number' }),
Expand Down Expand Up @@ -47,29 +46,20 @@ export const metadata = {
animation: (options: IAnimationMetadata) => ({ ...options, type: 'animation' }),
};

export type TInputMetadata<T = string> =
| ITextMetadata
| INumberMetadata
| ISliderMetadata
| ITextBoolMetadata
| ICheckboxGroupMetadata
| IRadioGroupMetadata
| IListMetadata<T>;

interface IBaseMetadata {
export interface IBaseMetadata {
Comment on lines -50 to +49
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This union type was hiding some potential bugs, so I moved to using the underlying base interface as our main export:

  • There was no way for other widgets to reference the base class for input types that don't have a hand-written type yet
  • The inference from TInputMetadata<unknown> was casting custom types to incorrect subtypes, like SponsorBanner's imagepicker input was casting as a IListMetadata since it was the only type with .options

label?: string;
tooltip?: string;
required?: boolean;
type?: string;
rules?: Rule[];
onChange?: (value: unknown) => void;
children?: Dictionary<TInputMetadata<unknown>>;
children?: Dictionary<IBaseMetadata>;
displayed?: boolean;
disabled?: boolean;
name?: string;
}

interface ITextMetadata extends IBaseMetadata {
export interface ITextMetadata extends IBaseMetadata {
value?: string;
isPassword?: boolean;
placeholder?: string;
Expand Down
224 changes: 224 additions & 0 deletions app/components-react/widgets/Credits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { Menu } from 'antd';
import { Services } from 'components-react/service-provider';
import Form from 'components-react/shared/inputs/Form';
import FormFactory from 'components-react/shared/inputs/FormFactory';
import React from 'react';
import { TPlatform } from 'services/platforms';
import { $t } from '../../services/i18n';
import { IBaseMetadata, metadata } from '../shared/inputs/metadata';
import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget';
import { WidgetLayout } from './common/WidgetLayout';

interface ICreditsState extends IWidgetCommonState {
data: {
themes: Dictionary<{ label: string }>;
settings: {
theme: string;
credit_title: string;
credit_subtitle: string;
background_color: string;
font_color: string;
font_size: 14;
font: string;
muted_chatters: string;
bits: boolean;
subscribers: boolean;
moderators: boolean;
donations: boolean;
followers: boolean;
bits_change: string;
donor_change: string;
followers_change: string;
mods_change: string;
subscribers_change: string;
delay_time: number;
roll_speed: number;
roll_time: number;
loop_credits: boolean;
};
};
}

type TCreditsMeta = PartialRec<
keyof ICreditsState['data']['settings'] | 'themes' | `_${string}`,
IBaseMetadata
>;
Comment on lines +42 to +45
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After struggling a bit to figure out how widget settings are synced via this system, I wanted to make it explicit in the types. This record type forces each section to either (a) use a synced value defined above, or (b) be explicit in its unsynced behavior via an underscore. See creditsMeta for more details.


function fromMeta(meta: TCreditsMeta): Record<string, IBaseMetadata> {
return meta as Record<string, IBaseMetadata>;
}

export function Credits() {
const {
isLoading,
settings,
creditsMeta,
fontMeta,
visualMeta,
updateSetting,
setSelectedTab,
selectedTab,
} = useCredits();

// use 1 column layout
return (
<WidgetLayout>
<Menu onClick={e => setSelectedTab(e.key)} selectedKeys={[selectedTab]}>
<Menu.Item key="credits">{$t('Manage Credits')}</Menu.Item>
<Menu.Item key="font">{$t('Font Settings')}</Menu.Item>
<Menu.Item key="visual">{$t('Visual Settings')}</Menu.Item>
</Menu>
<Form>
{!isLoading && selectedTab === 'credits' && (
<FormFactory metadata={creditsMeta} values={settings} onChange={updateSetting} />
)}
{!isLoading && selectedTab === 'font' && (
<FormFactory metadata={fontMeta} values={settings} onChange={updateSetting} />
)}
{!isLoading && selectedTab === 'visual' && (
<FormFactory metadata={visualMeta} values={settings} onChange={updateSetting} />
)}
</Form>
</WidgetLayout>
);
}

export class CreditsModule extends WidgetModule<ICreditsState> {
get UserService() {
return Services.UserService;
}

get includesByPlatform() {
const platform = this.UserService.views.platform?.type;
const baseEvents = {
donations: metadata.bool({ label: $t('Show Donations') }),
};
const platformEvents: PartialRec<TPlatform, any> = {
twitch: {
followers: metadata.bool({ label: $t('Show Followers') }),
subscribers: metadata.bool({ label: $t('Show Subscribers') }),
bits: metadata.bool({ label: $t('Show Cheers') }),
moderators: metadata.bool({ label: $t('Show Moderators') }),
},
youtube: {
subscriptions: metadata.bool({ label: $t('Show Subscriptions') }),
sponsors: metadata.bool({ label: $t('Show Members') }),
superchats: metadata.bool({ label: $t('Show Super Chats') }),
},
};
if (!platform || !platformEvents[platform]) return fromMeta(baseEvents);
return fromMeta({ ...platformEvents[platform], ...baseEvents });
}

get titlesByPlatform() {
const platform = this.UserService.views.platform?.type;
const baseEvents = {
donor_change: metadata.text({ label: $t('Donors') }),
};
const platformEvents: PartialRec<TPlatform, any> = {
twitch: {
followers_change: metadata.text({ label: $t('Followers') }),
subscribers_change: metadata.text({ label: $t('Subscribers & Resubs') }),
bits_change: metadata.text({ label: $t('Cheers') }),
mods_change: metadata.text({ label: $t('Moderators') }),
},
youtube: {
subscriptions_change: metadata.text({ label: $t('Subscriptions') }),
sponsors_change: metadata.text({ label: $t('Members') }),
superchats_change: metadata.text({ label: $t('Super Chats') }),
},
};
if (!platform || !platformEvents[platform]) return fromMeta(baseEvents);
return fromMeta({ ...platformEvents[platform], ...baseEvents });
}

get creditsMeta() {
return fromMeta({
credit_title: metadata.text({ label: $t('Credit Title') }),
credit_subtitle: metadata.text({
label: $t('Credit Subtitle'),
tooltip:
$t('When the credits roll, this will be the format of the subtitle. Available tokens:') +
'\r' +
[
'{total_donated_amount}',
'{total_cheer_amount}',
'{top_donor}',
'{top_donated_amount}',
'{top_cheer_donor}',
'{username}',
'{top_cheer_amount}',
'{new_subscriber_count}',
'{new_follower_count}',
].join(', '),
}),
_includes: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the TCreditsMeta magic helps make the config explicit. If this were named includes, TypeScript will now emit the following:

Diagnostics:
Object literal may only specify known properties,
and 'includes' does not exist in type 'Partial<Record<
  "theme" | "credit_title" | "credit_subtitle" | "background_color"
    | "font_color" | "font_size" | "font" | "muted_chatters" | "bits"
    | "subscribers" | "moderators" | "donations" | ... 11 more ...
    | `_${string}`,
  IBaseMetadata>
>'. [2353]

Same thing for typos like font_size vs fontsize, and even widget API mismatches like Credits' font_size vs Event List's text_size.

type: 'checkboxGroup',
label: $t('Includes'),
children: this.includesByPlatform,
},
...this.titlesByPlatform,
});
}

get fontMeta() {
return fromMeta({
font_color: metadata.color({
label: $t('Text Color'),
tooltip: $t('A hex code for the base text color.'),
}),
font: metadata.fontFamily({ label: $t('Font') }),
font_size: metadata.fontSize({
label: $t('Font Size'),
min: 10,
max: 80,
tooltip: $t(
'The font size in pixels. Reasonable size typically ranges between 24px and 48px.',
),
}),
});
}

get visualMeta() {
return fromMeta({
theme: metadata.list({
label: $t('Theme'),
options: Object.entries(this.widgetData?.themes || {}).map(([theme, val]) => ({
label: val.label,
value: theme,
})),
}),
background_color: metadata.color({
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review question: how does the new system handle previews? I didn't see anywhere in the reference code where event list was handling preview.

label: $t('Background Color'),
tooltip: $t(
'A hex code for the widget background. This is for preview purposes only. It will not be shown in your stream.',
),
}),
delay_time: metadata.slider({
label: $t('Delay Time'),
tooltip: $t('Wait time before rerunning the credit reel.'),
min: 0,
max: 10,
step: 1,
}),
roll_speed: metadata.slider({
label: $t('Roll Speed'),
tooltip: $t('Speed of the rolling credits.'),
min: 1,
max: 5,
step: 1,
}),
roll_time: metadata.slider({
label: $t('Roll Time'),
tooltip: $t('Duration of the rolling credits.'),
min: 15,
max: 150,
step: 5,
}),
});
}
}

function useCredits() {
return useWidget<CreditsModule>();
}
19 changes: 9 additions & 10 deletions app/components-react/widgets/EventList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React from 'react';
import { Menu } from 'antd';
import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget';
import { WidgetLayout } from './common/WidgetLayout';
import { $t } from '../../services/i18n';
import { metadata } from '../shared/inputs/metadata';
import FormFactory from 'components-react/shared/inputs/FormFactory';
import { UserService } from 'app-services';
import { Services } from 'components-react/service-provider';
import { TPlatform } from 'services/platforms';
import Form from 'components-react/shared/inputs/Form';
import FormFactory from 'components-react/shared/inputs/FormFactory';
import React from 'react';
import { TPlatform } from 'services/platforms';
import { $t } from '../../services/i18n';
import { metadata } from '../shared/inputs/metadata';
import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget';
import { WidgetLayout } from './common/WidgetLayout';

interface IEventListState extends IWidgetCommonState {
data: {
Expand Down Expand Up @@ -131,7 +130,7 @@ export class EventListModule extends WidgetModule<IEventListState> {
show_sub_tiers: metadata.bool({ label: $t('Show Sub Tiers') }),
},
};
if (!platform) return baseEvents;
if (!platform || !platformEvents[platform]) return baseEvents;
return { ...platformEvents[platform], ...baseEvents };
}

Expand Down Expand Up @@ -181,7 +180,7 @@ export class EventListModule extends WidgetModule<IEventListState> {
return {
theme: metadata.list({
label: $t('Theme'),
options: Object.entries(this.widgetData?.themes || []).map(([theme, val]) => ({
options: Object.entries(this.widgetData?.themes || {}).map(([theme, val]) => ({
label: val.label,
value: theme,
})),
Expand Down
4 changes: 2 additions & 2 deletions app/components-react/widgets/SponsorBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,15 @@ export class SponsorBannerModule extends WidgetModule<ISponsorBannerState> {
{ label: $t('Double'), value: 'double' },
],
children: {
layout: {
layout: metadata.any({
type: 'imagepicker',
label: $t('Image Layout'),
options: [
{ label: '', value: 'side', image: $i('images/layout-image-side.png') },
{ label: '', value: 'above', image: $i('images/layout-image-above.png') },
],
displayed: this.settings.placement_options === 'double',
},
}),
},
}),
};
Expand Down
4 changes: 2 additions & 2 deletions app/components-react/widgets/common/WidgetWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useWidgetRoot, WidgetModule } from './useWidget';
import { GenericGoal, GenericGoalModule } from '../GenericGoal';
import { ChatBox, ChatBoxModule } from '../ChatBox';
// ChatHighlight
// Credits
import { Credits, CreditsModule } from '../Credits';
import { DonationTicker, DonationTickerModule } from '../DonationTicker';
import { EmoteWall, EmoteWallModule } from '../EmoteWall';
import { EventList, EventListModule } from '../EventList';
Expand Down Expand Up @@ -39,7 +39,7 @@ export const components = {
SubscriberGoal: [GenericGoal, GenericGoalModule],
ChatBox: [ChatBox, ChatBoxModule],
// ChatHighlight
// Credits
Credits: [Credits, CreditsModule],
DonationTicker: [DonationTicker, DonationTickerModule],
EmoteWall: [EmoteWall, EmoteWallModule],
EventList: [EventList, EventListModule],
Expand Down
Loading
Loading