Skip to content

Commit 85224fa

Browse files
Add random favorite requests
1 parent 1801033 commit 85224fa

24 files changed

+438
-62
lines changed

src/extension/panel/app.tsx

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ type PanelViewerRequestSubmitInput =
279279
requestKind: "regular" | "vip";
280280
requestMode: "random" | "choice";
281281
vipTokenCost?: number;
282+
}
283+
| {
284+
requestKind: "regular" | "vip";
285+
requestMode: "favorite";
286+
vipTokenCost?: number;
282287
};
283288

284289
type PanelDropTargetState = {
@@ -939,10 +944,14 @@ function ExtensionPanelAppContent(props: {
939944
requestMode: "catalog",
940945
requestedPath: input.requestedPath,
941946
}
942-
: {
943-
query: input.query.trim(),
944-
requestMode: input.requestMode,
945-
}),
947+
: "query" in input
948+
? {
949+
query: input.query.trim(),
950+
requestMode: input.requestMode,
951+
}
952+
: {
953+
requestMode: input.requestMode,
954+
}),
946955
requestKind: input.requestKind,
947956
vipTokenCost: input.vipTokenCost,
948957
itemId: editingRequestItemId ?? undefined,
@@ -2113,7 +2122,10 @@ export function ExtensionPanelModeratorPreview() {
21132122
}
21142123

21152124
async function handleSubmitRequest(input: PanelViewerRequestSubmitInput) {
2116-
const normalizedQuery = "query" in input ? input.query.trim() : null;
2125+
const normalizedQuery =
2126+
"query" in input && typeof input.query === "string"
2127+
? input.query.trim()
2128+
: null;
21172129
const song =
21182130
"songId" in input
21192131
? (searchResults?.items.find(
@@ -2162,10 +2174,14 @@ export function ExtensionPanelModeratorPreview() {
21622174
requestedPath:
21632175
"songId" in input ? input.requestedPath : undefined,
21642176
}
2165-
: {
2166-
query: normalizedQuery ?? "",
2167-
requestMode: input.requestMode,
2168-
}),
2177+
: "query" in input
2178+
? {
2179+
query: normalizedQuery ?? "",
2180+
requestMode: input.requestMode,
2181+
}
2182+
: {
2183+
requestMode: input.requestMode,
2184+
}),
21692185
requestKind: input.requestKind,
21702186
vipTokenCost: input.vipTokenCost,
21712187
replaceExisting: false,
@@ -3239,19 +3255,47 @@ function PanelSpecialRequestControls(props: {
32393255
pendingAction: string | null;
32403256
isEditingRequest: boolean;
32413257
onSubmit: (
3242-
requestMode: "random" | "choice",
3258+
requestMode: "random" | "favorite" | "choice",
32433259
requestKind: "regular" | "vip"
32443260
) => void;
32453261
}) {
32463262
const { t } = useLocaleTranslation("extension");
32473263
const normalizedQuery = props.query.trim();
3248-
const regularDisabledReason = getPanelSpecialRequestDisabledReason({
3264+
const randomDisabledReason = getPanelSpecialRequestDisabledReason({
3265+
query: normalizedQuery,
3266+
requestMode: "random",
3267+
canRequest: props.canRequest,
3268+
t,
3269+
});
3270+
const randomVipDisabledReason = getPanelSpecialRequestDisabledReason({
32493271
query: normalizedQuery,
3272+
requestMode: "random",
3273+
canRequest: props.canVipRequest,
3274+
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
3275+
t,
3276+
});
3277+
const choiceDisabledReason = getPanelSpecialRequestDisabledReason({
3278+
query: normalizedQuery,
3279+
requestMode: "choice",
32503280
canRequest: props.canRequest,
32513281
t,
32523282
});
3253-
const vipDisabledReason = getPanelSpecialRequestDisabledReason({
3283+
const choiceVipDisabledReason = getPanelSpecialRequestDisabledReason({
32543284
query: normalizedQuery,
3285+
requestMode: "choice",
3286+
canRequest: props.canVipRequest,
3287+
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
3288+
t,
3289+
});
3290+
const favoriteDisabledReason = getPanelSpecialRequestDisabledReason({
3291+
query: normalizedQuery,
3292+
requestMode: "favorite",
3293+
canRequest: props.canRequest,
3294+
t,
3295+
});
3296+
const favoriteVipDisabledReason = getPanelSpecialRequestDisabledReason({
3297+
query: normalizedQuery,
3298+
requestMode: "favorite",
32553299
canRequest: props.canVipRequest,
32563300
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
32573301
t,
@@ -3265,8 +3309,8 @@ function PanelSpecialRequestControls(props: {
32653309
<div className="grid gap-2">
32663310
<PanelSpecialRequestRow
32673311
label={t("requests.randomSong")}
3268-
disabledReason={regularDisabledReason}
3269-
vipDisabledReason={vipDisabledReason}
3312+
disabledReason={randomDisabledReason}
3313+
vipDisabledReason={randomVipDisabledReason}
32703314
busy={props.pendingAction != null}
32713315
regularPending={
32723316
props.pendingAction ===
@@ -3288,10 +3332,33 @@ function PanelSpecialRequestControls(props: {
32883332
onRegularClick={() => props.onSubmit("random", "regular")}
32893333
onVipClick={() => props.onSubmit("random", "vip")}
32903334
/>
3335+
<PanelSpecialRequestRow
3336+
label={t("requests.randomFavorite")}
3337+
disabledReason={favoriteDisabledReason}
3338+
vipDisabledReason={favoriteVipDisabledReason}
3339+
busy={props.pendingAction != null}
3340+
regularPending={
3341+
props.pendingAction ===
3342+
getPanelViewerRequestActionKey({
3343+
requestMode: "favorite",
3344+
requestKind: "regular",
3345+
})
3346+
}
3347+
vipPending={
3348+
props.pendingAction ===
3349+
getPanelViewerRequestActionKey({
3350+
requestMode: "favorite",
3351+
requestKind: "vip",
3352+
})
3353+
}
3354+
isEditingRequest={props.isEditingRequest}
3355+
onRegularClick={() => props.onSubmit("favorite", "regular")}
3356+
onVipClick={() => props.onSubmit("favorite", "vip")}
3357+
/>
32913358
<PanelSpecialRequestRow
32923359
label={t("requests.streamerChoice")}
3293-
disabledReason={regularDisabledReason}
3294-
vipDisabledReason={vipDisabledReason}
3360+
disabledReason={choiceDisabledReason}
3361+
vipDisabledReason={choiceVipDisabledReason}
32953362
busy={props.pendingAction != null}
32963363
regularPending={
32973364
props.pendingAction ===
@@ -4243,16 +4310,19 @@ function getPanelViewerRequestActionKey(input: PanelViewerRequestSubmitInput) {
42434310
return `${input.songId}:${input.requestKind}:${input.requestedPath ?? "none"}`;
42444311
}
42454312

4246-
return `special:${input.requestMode}:${input.requestKind}:${input.query.trim().toLowerCase()}`;
4313+
return "query" in input
4314+
? `special:${input.requestMode}:${input.requestKind}:${input.query.trim().toLowerCase()}`
4315+
: `special:${input.requestMode}:${input.requestKind}`;
42474316
}
42484317

42494318
function getPanelSpecialRequestDisabledReason(input: {
42504319
query: string;
4320+
requestMode: "random" | "favorite" | "choice";
42514321
canRequest: boolean;
42524322
fallbackReason?: string;
42534323
t: TFunction;
42544324
}) {
4255-
if (input.query.length < 2) {
4325+
if (input.requestMode !== "favorite" && input.query.length < 2) {
42564326
return input.t("search.typeAtLeastTwo");
42574327
}
42584328

src/extension/panel/demo.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function applyDemoViewerRequestMutation(input: {
107107
viewerProfile: PanelDemoViewerProfile;
108108
song?: Record<string, unknown>;
109109
query?: string;
110-
requestMode?: "catalog" | "random" | "choice";
110+
requestMode?: "catalog" | "random" | "favorite" | "choice";
111111
requestKind: "regular" | "vip";
112112
requestedPath?: string;
113113
vipTokenCost?: number;
@@ -122,7 +122,9 @@ export function applyDemoViewerRequestMutation(input: {
122122
const songId =
123123
requestMode === "choice"
124124
? `preview-choice-${now}`
125-
: (getString(songRecord, "id") ?? `preview-song-${now}`);
125+
: requestMode === "favorite" && !getString(songRecord, "id")
126+
? `preview-favorite-${now}`
127+
: (getString(songRecord, "id") ?? `preview-song-${now}`);
126128
const nextId = input.nextId ?? `preview-request-${songId}-${now}`;
127129
const replaceTarget =
128130
input.replaceItemId != null
@@ -154,7 +156,10 @@ export function applyDemoViewerRequestMutation(input: {
154156
songTitle:
155157
requestMode === "choice"
156158
? "Streamer choice"
157-
: (getString(songRecord, "title") ?? "Unknown song"),
159+
: requestMode === "favorite" &&
160+
!getString(songRecord, "title")
161+
? "Random favorite"
162+
: (getString(songRecord, "title") ?? "Unknown song"),
158163
songArtist:
159164
requestMode === "choice"
160165
? null
@@ -230,7 +235,9 @@ export function applyDemoViewerRequestMutation(input: {
230235
songTitle:
231236
requestMode === "choice"
232237
? "Streamer choice"
233-
: (getString(songRecord, "title") ?? "Unknown song"),
238+
: requestMode === "favorite" && !getString(songRecord, "title")
239+
? "Random favorite"
240+
: (getString(songRecord, "title") ?? "Unknown song"),
234241
songArtist:
235242
requestMode === "choice" ? null : getString(songRecord, "artist"),
236243
songAlbum: requestMode === "choice" ? null : getString(songRecord, "album"),

src/lib/eventsub/chat-message.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ function buildCatalogSearchInput(input: {
502502
pageSize: number;
503503
state: EventSubChatState;
504504
allowBlacklistOverride: boolean;
505+
favoriteChannelId?: string;
505506
}) {
506507
const blacklistFilters =
507508
input.allowBlacklistOverride || !input.state.settings.blacklistEnabled
@@ -529,6 +530,7 @@ function buildCatalogSearchInput(input: {
529530
query: input.query,
530531
page: input.page,
531532
pageSize: input.pageSize,
533+
favoriteChannelId: input.favoriteChannelId,
532534
restrictToOfficial: !!input.state.settings.onlyOfficialDlc,
533535
allowedTuningsFilter: getArraySetting(
534536
input.state.settings.allowedTuningsJson
@@ -595,13 +597,15 @@ async function resolveChatRandomMatch(input: {
595597
requesterContext: Parameters<typeof isRequesterAllowed>[1];
596598
allowBlacklistOverride: boolean;
597599
requestedPaths: string[];
600+
favoriteChannelId?: string;
598601
}) {
599602
const baseSearchInput = buildCatalogSearchInput({
600603
query: input.query,
601604
page: 1,
602605
pageSize: 1,
603606
state: input.state,
604607
allowBlacklistOverride: input.allowBlacklistOverride,
608+
favoriteChannelId: input.favoriteChannelId,
605609
});
606610
const filteredSearch = await input.deps.searchSongs(
607611
input.env,
@@ -860,7 +864,7 @@ function requestedPathsMatch(left: string[], right: string[]) {
860864
function getRequestedQueryForStorage(input: {
861865
parsedQuery?: string | null;
862866
normalizedQuery: string;
863-
requestMode: "catalog" | "random" | "choice";
867+
requestMode: "catalog" | "random" | "favorite" | "choice";
864868
requestedPaths: string[];
865869
}) {
866870
if (input.requestMode === "choice") {
@@ -1725,7 +1729,7 @@ export async function processEventSubChatMessage(input: {
17251729
requestedPaths,
17261730
});
17271731

1728-
if (!normalizedQuery) {
1732+
if (!normalizedQuery && requestMode !== "favorite") {
17291733
await deps.createRequestLog(env, {
17301734
channelId: channel.id,
17311735
twitchMessageId: event.messageId,
@@ -1791,6 +1795,19 @@ export async function processEventSubChatMessage(input: {
17911795
});
17921796
firstMatch = randomMatch.firstMatch;
17931797
firstRejectedMatch = randomMatch.firstRejectedMatch;
1798+
} else if (requestMode === "favorite") {
1799+
const favoriteMatch = await resolveChatRandomMatch({
1800+
env,
1801+
deps,
1802+
query: "",
1803+
state,
1804+
requesterContext,
1805+
allowBlacklistOverride,
1806+
requestedPaths,
1807+
favoriteChannelId: channel.id,
1808+
});
1809+
firstMatch = favoriteMatch.firstMatch;
1810+
firstRejectedMatch = favoriteMatch.firstRejectedMatch;
17941811
} else if (requestedSourceSongId != null) {
17951812
firstMatch = await deps.getCatalogSongBySourceId(
17961813
env,
@@ -1942,7 +1959,7 @@ export async function processEventSubChatMessage(input: {
19421959
return { body: "Rejected", status: 202 };
19431960
}
19441961

1945-
if (requestMode === "random") {
1962+
if (requestMode === "random" || requestMode === "favorite") {
19461963
await deps.createRequestLog(env, {
19471964
channelId: channel.id,
19481965
twitchMessageId: event.messageId,
@@ -1952,15 +1969,23 @@ export async function processEventSubChatMessage(input: {
19521969
rawMessage: event.rawMessage,
19531970
normalizedQuery: parsed.query,
19541971
outcome: "rejected",
1955-
outcomeReason: "random_match_missing",
1972+
outcomeReason:
1973+
requestMode === "favorite"
1974+
? "favorite_match_missing"
1975+
: "random_match_missing",
19561976
});
19571977
await deps.sendChatReply(env, {
19581978
channelId: channel.id,
19591979
broadcasterUserId: channel.twitchChannelId,
1960-
message: t("replies.randomNotFound", {
1961-
mention: mention(requesterIdentity.login),
1962-
query: unmatchedQuery,
1963-
}),
1980+
message:
1981+
requestMode === "favorite"
1982+
? t("replies.favoriteNotFound", {
1983+
mention: mention(requesterIdentity.login),
1984+
})
1985+
: t("replies.randomNotFound", {
1986+
mention: mention(requesterIdentity.login),
1987+
query: unmatchedQuery,
1988+
}),
19641989
});
19651990
return { body: "Rejected", status: 202 };
19661991
}

src/lib/i18n/resources/en/bot.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"commands": {
1919
"how": {
20-
"commands": "Commands: {commandPrefix}sr artist - song; {commandPrefix}sr artist *random; {commandPrefix}sr artist *choice; {commandPrefix}vip; {commandPrefix}vip artist - song; {commandPrefix}vip artist - song *2; {commandPrefix}edit #2 artist - song; {commandPrefix}remove reg|vip|all; {commandPrefix}position.",
20+
"commands": "Commands: {commandPrefix}sr artist - song; {commandPrefix}sr artist *random; {commandPrefix}sr favorite; {commandPrefix}sr artist *choice; {commandPrefix}vip; {commandPrefix}vip artist - song; {commandPrefix}vip artist - song *2; {commandPrefix}edit #2 artist - song; {commandPrefix}remove reg|vip|all; {commandPrefix}position.",
2121
"partRequests": "Part requests: add {modifiers} to {commandPrefix}sr, {commandPrefix}vip, or {commandPrefix}edit.",
2222
"partRequestsVip": "Part requests: add {modifiers} to {commandPrefix}sr, {commandPrefix}vip, or {commandPrefix}edit. They require {countText}.",
2323
"browse": "Browse the track list and request songs here: {url}"
@@ -84,6 +84,7 @@
8484
"requestQueryMissing": "{mention} include an artist or song before using request modifiers.",
8585
"songLookupFailed": "{mention} I ran into a problem searching for that song. Please try again.",
8686
"randomNotFound": "{mention} I couldn't find an allowed random match for \"{query}\".",
87+
"favoriteNotFound": "{mention} I couldn't find an allowed favorite from this channel right now.",
8788
"rejectedSongBlocked": "{mention} I cannot add that song to the playlist.",
8889
"rejectedSongReason": "{mention} {reason}",
8990
"choiceAlreadyActive": "{mention} that streamer choice request is already in your active requests.",

src/lib/i18n/resources/en/extension.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"offRightNow": "Requests are off right now.",
3636
"quick": "Quick request",
3737
"randomSong": "Random song",
38+
"randomFavorite": "Random favorite",
3839
"streamerChoice": "Streamer choice",
3940
"limitReached": "You already have {count} active {count, plural, one {request} other {requests}} in this playlist.",
4041
"noPermission": "You cannot request songs right now.",

src/lib/i18n/resources/en/playlist.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,17 @@
231231
"chooseMode": "Choose mode",
232232
"chooseType": "Choose type",
233233
"random": "Random",
234+
"favorite": "Favorite",
234235
"choice": "Choice",
235236
"regular": "Regular",
236237
"add": "Add",
237238
"addVip": "Add VIP",
238239
"edit": "Edit",
239240
"editVip": "Edit VIP",
240241
"adding": "Adding...",
242+
"favoritePlaceholder": "Not used for favorites",
241243
"randomHelp": "Adds a random song from the matching songs for this artist.",
244+
"favoriteHelp": "Adds a random song from this channel's saved favorites.",
242245
"choiceHelp": "Adds a streamer choice request for this artist."
243246
},
244247
"manageActions": {

src/lib/i18n/resources/es/bot.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"commands": {
1919
"how": {
20-
"commands": "Commands: {commandPrefix}sr artist - song; {commandPrefix}sr artist *random; {commandPrefix}sr artist *choice; {commandPrefix}vip; {commandPrefix}vip artist - song; {commandPrefix}vip artist - song *2; {commandPrefix}edit #2 artist - song; {commandPrefix}remove reg|vip|all; {commandPrefix}position.",
20+
"commands": "Commands: {commandPrefix}sr artist - song; {commandPrefix}sr artist *random; {commandPrefix}sr favorite; {commandPrefix}sr artist *choice; {commandPrefix}vip; {commandPrefix}vip artist - song; {commandPrefix}vip artist - song *2; {commandPrefix}edit #2 artist - song; {commandPrefix}remove reg|vip|all; {commandPrefix}position.",
2121
"partRequests": "Solicitudes de parte: agrega {modifiers} a {commandPrefix}sr, {commandPrefix}vip o {commandPrefix}edit.",
2222
"partRequestsVip": "Solicitudes de parte: agrega {modifiers} a {commandPrefix}sr, {commandPrefix}vip o {commandPrefix}edit. Requieren {countText}.",
2323
"browse": "Browse the track list and request songs here: {url}"
@@ -84,6 +84,7 @@
8484
"requestQueryMissing": "{mention} incluye un artista o una canción antes de usar modificadores de solicitud.",
8585
"songLookupFailed": "{mention} I ran into a problem searching for that song. Please try again.",
8686
"randomNotFound": "{mention} no pude encontrar una coincidencia aleatoria permitida para \"{query}\".",
87+
"favoriteNotFound": "{mention} no pude encontrar un favorito permitido de este canal en este momento.",
8788
"rejectedSongBlocked": "{mention} no puedo agregar esa canción a la lista.",
8889
"rejectedSongReason": "{mention} {reason}",
8990
"choiceAlreadyActive": "{mention} that streamer choice request is already in your active requests.",

0 commit comments

Comments
 (0)