From adce1d2099bb142fd5d2c031d884124c3bd4344e Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:33:05 +0300 Subject: [PATCH] Show friends list LIVE badge only when there is a stream to watch and link to streams --- app/components/SideNav.module.css | 1 + app/components/match-page/MatchTimeline.tsx | 2 +- .../friends/components/FriendMenu.tsx | 36 +++++- app/features/friends/friends-constants.ts | 37 +++--- app/features/friends/friends-utils.server.ts | 107 +++++++++++++++++- .../friends/loaders/friends.server.ts | 26 +++-- app/features/sidebar/core/sidebar.server.ts | 36 +++--- .../tournament-bracket/core/Tournament.ts | 1 + locales/da/friends.json | 3 + locales/da/q.json | 2 +- locales/de/friends.json | 3 + locales/de/q.json | 2 +- locales/en/friends.json | 3 + locales/en/q.json | 2 +- locales/es-ES/friends.json | 3 + locales/es-ES/q.json | 2 +- locales/es-US/friends.json | 3 + locales/es-US/q.json | 2 +- locales/fr-CA/friends.json | 3 + locales/fr-CA/q.json | 2 +- locales/fr-EU/friends.json | 3 + locales/fr-EU/q.json | 2 +- locales/he/friends.json | 3 + locales/he/q.json | 2 +- locales/it/friends.json | 3 + locales/it/q.json | 2 +- locales/ja/friends.json | 3 + locales/ja/q.json | 2 +- locales/ko/friends.json | 3 + locales/ko/q.json | 2 +- locales/nl/friends.json | 3 + locales/nl/q.json | 2 +- locales/pl/friends.json | 3 + locales/pl/q.json | 2 +- locales/pt-BR/friends.json | 3 + locales/pt-BR/q.json | 2 +- locales/ru/friends.json | 3 + locales/ru/q.json | 2 +- locales/zh/friends.json | 3 + locales/zh/q.json | 2 +- 40 files changed, 261 insertions(+), 65 deletions(-) diff --git a/app/components/SideNav.module.css b/app/components/SideNav.module.css index 3e2d4a4e9..b36a14270 100644 --- a/app/components/SideNav.module.css +++ b/app/components/SideNav.module.css @@ -268,6 +268,7 @@ .listLinkSubtitleRow { display: flex; align-items: center; + gap: var(--s-1-5); width: 100%; color: var(--color-text-high); } diff --git a/app/components/match-page/MatchTimeline.tsx b/app/components/match-page/MatchTimeline.tsx index 5c022a127..bfb78c12c 100644 --- a/app/components/match-page/MatchTimeline.tsx +++ b/app/components/match-page/MatchTimeline.tsx @@ -185,7 +185,7 @@ function TimelineHeader({ ) : null} {isOngoing ? ( - {t("q:match.timeline.live")} + {t("q:match.timeline.ongoing")} ) : null} diff --git a/app/features/friends/components/FriendMenu.tsx b/app/features/friends/components/FriendMenu.tsx index 5b394de32..635fa12d7 100644 --- a/app/features/friends/components/FriendMenu.tsx +++ b/app/features/friends/components/FriendMenu.tsx @@ -9,10 +9,12 @@ import { SendouMenuItem, SendouMenuSection, } from "~/components/elements/Menu"; +import { TwitchIcon } from "~/components/icons/Twitch"; import { ListButton } from "~/components/SideNav"; import { + type FriendActivityBadge, type FriendActivityType, - isLiveFriendActivity, + friendActivityBadge, } from "~/features/friends/friends-constants"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { @@ -23,6 +25,11 @@ import { tournamentSubsPage, } from "~/utils/urls"; +const ACTIVITY_BADGE_TRANSLATION_KEY = { + MATCH: "friends:friendsList.inMatch", + NEXT: "friends:friendsList.nextMatch", +} as const satisfies Record; + export function FriendMenu({ discordId, discordAvatar, @@ -34,6 +41,7 @@ export function FriendMenu({ activityType, matchId, tournamentId, + streamUrl, friendshipId, friendshipCreatedAt, onNavigate, @@ -48,6 +56,7 @@ export function FriendMenu({ activityType: FriendActivityType | null; matchId: number | null; tournamentId: number | null; + streamUrl: string | null; friendshipId?: number; friendshipCreatedAt?: number | null; onNavigate?: () => void; @@ -67,7 +76,7 @@ export function FriendMenu({ }) : null; - const isLive = isLiveFriendActivity(activityType); + const activityBadge = friendActivityBadge(activityType); const activity = resolveActivity({ activityType, matchId, tournamentId }); return ( @@ -77,8 +86,14 @@ export function FriendMenu({ {name} @@ -88,6 +103,17 @@ export function FriendMenu({ } onAction={onNavigate}> {t("friends:friendsList.viewUserPage")} + {streamUrl ? ( + } + onAction={onNavigate} + > + {t("friends:friendsList.watchStream")} + + ) : null} {activity?.type === "join-sendouq" ? ( } @@ -189,7 +215,7 @@ function resolveActivity(friend: { }), } as const) : null; - case "TOURNAMENT_PLAYING": + case "TOURNAMENT_WAITING": return friend.tournamentId ? ({ type: "view-tournament", diff --git a/app/features/friends/friends-constants.ts b/app/features/friends/friends-constants.ts index 1fe64d496..7500215cf 100644 --- a/app/features/friends/friends-constants.ts +++ b/app/features/friends/friends-constants.ts @@ -8,30 +8,33 @@ export const SENDOUQ_ACTIVITY_LABEL = "SendouQ"; export type FriendActivityType = | "SENDOUQ_MATCH" | "TOURNAMENT_MATCH" - | "TOURNAMENT_PLAYING" + | "TOURNAMENT_WAITING" | "SENDOUQ" | "TOURNAMENT_SUB"; -/** - * Whether the activity represents a friend currently playing (in a live match - * or otherwise busy in a running tournament) as opposed to looking for members. - */ -export function isLiveFriendActivity(type: FriendActivityType | null) { - return ( - type === "SENDOUQ_MATCH" || - type === "TOURNAMENT_MATCH" || - type === "TOURNAMENT_PLAYING" - ); +export type FriendActivityBadge = "MATCH" | "NEXT"; + +const ACTIVITY_BADGE: Record = { + SENDOUQ_MATCH: "MATCH", + TOURNAMENT_MATCH: "MATCH", + TOURNAMENT_WAITING: "NEXT", + SENDOUQ: null, + TOURNAMENT_SUB: null, +}; + +export function friendActivityBadge(type: FriendActivityType | null) { + if (!type) return null; + + return ACTIVITY_BADGE[type]; +} + +export function isInProgressFriendActivity(type: FriendActivityType | null) { + return friendActivityBadge(type) !== null; } -/** - * Sort value used to order friends by how interesting their activity is. - * Looking for members ranks highest (others can act on it), then live activity, - * then no activity. - */ export function friendActivitySortValue(type: FriendActivityType | null) { if (type === "SENDOUQ") return 4; if (type === "TOURNAMENT_SUB") return 3; - if (isLiveFriendActivity(type)) return 2; + if (isInProgressFriendActivity(type)) return 2; return 0; } diff --git a/app/features/friends/friends-utils.server.ts b/app/features/friends/friends-utils.server.ts index 25bacaf8e..0e4c4febd 100644 --- a/app/features/friends/friends-utils.server.ts +++ b/app/features/friends/friends-utils.server.ts @@ -1,8 +1,13 @@ import { groupExpiryStatus } from "~/features/sendouq/core/groups"; import { SendouQ } from "~/features/sendouq/core/SendouQ.server"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; +import { cachedStreams } from "~/features/sendouq-streams/core/streams.server"; import { RunningTournaments } from "~/features/tournament-bracket/core/RunningTournaments.server"; -import type { TournamentTeamMemberProgressStatus } from "~/features/tournament-bracket/core/Tournament"; +import type { + Tournament, + TournamentTeamMemberProgressStatus, +} from "~/features/tournament-bracket/core/Tournament"; +import { twitchUrl } from "~/utils/urls"; import { type FriendActivityType, SENDOUQ_ACTIVITY_LABEL, @@ -14,6 +19,8 @@ export interface FriendActivity { badge: string | null; matchId: number | null; tournamentId: number | null; + /** Set when the friend's current match can be watched, making the activity show up as "LIVE". */ + streamUrl: string | null; } const TOURNAMENT_STATUS_IS_IN_PROGRESS: Record< @@ -25,15 +32,32 @@ const TOURNAMENT_STATUS_IS_IN_PROGRESS: Record< WAITING_FOR_CAST: true, WAITING_FOR_ROUND: true, WAITING_FOR_GROUPS: true, - // to counter 2 day tournaments showing LIVE in between + // to counter 2 day tournaments showing as in progress in between WAITING_FOR_BRACKET: false, CHECKIN: false, THANKS_FOR_PLAYING: false, }; +/** + * Twitch account streaming each ongoing SendouQ match, keyed by match id. Resolved + * once per request as activity is resolved separately for every friend. + */ +export async function resolveSendouQMatchStreams() { + const streams = await cachedStreams(); + + const result = new Map(); + for (const { match, stream } of streams) { + if (!stream.twitchUserName || result.has(match.id)) continue; + + result.set(match.id, stream.twitchUserName); + } + + return result; +} + /** * Resolves what a friend is currently doing for display in the friends list, - * prioritizing in-progress activity (a live SendouQ or tournament match) over + * prioritizing in-progress activity (an ongoing SendouQ or tournament match) over * looking-for-members activity. */ export function resolveFriendActivity({ @@ -42,22 +66,27 @@ export function resolveFriendActivity({ tournamentName, teamMemberCount, tournamentMinTeamSize, + sendouQMatchStreams, }: { friendId: number; tournamentId: number | null; tournamentName: string | null; teamMemberCount: number | null; tournamentMinTeamSize: number | null; + sendouQMatchStreams: ReadonlyMap; }): FriendActivity { const ownGroup = SendouQ.findOwnGroup(friendId); if (ownGroup?.matchId) { + const twitchAccount = sendouQMatchStreams.get(ownGroup.matchId); + return { type: "SENDOUQ_MATCH", subtitle: SENDOUQ_ACTIVITY_LABEL, badge: null, matchId: ownGroup.matchId, tournamentId: null, + streamUrl: twitchAccount ? twitchUrl(twitchAccount) : null, }; } @@ -75,6 +104,7 @@ export function resolveFriendActivity({ badge: `${ownGroup.members.length}/${FULL_GROUP_SIZE}`, matchId: null, tournamentId: null, + streamUrl: null, }; } @@ -85,6 +115,7 @@ export function resolveFriendActivity({ badge: `${teamMemberCount ?? 1}/${tournamentMinTeamSize ?? FULL_GROUP_SIZE}`, matchId: null, tournamentId, + streamUrl: null, }; } @@ -94,6 +125,7 @@ export function resolveFriendActivity({ badge: null, matchId: null, tournamentId: null, + streamUrl: null, }; } @@ -102,14 +134,79 @@ function resolveTournamentActivity(friendId: number): FriendActivity | null { const status = tournament.teamMemberOfProgressStatus({ id: friendId }); if (!status || !TOURNAMENT_STATUS_IS_IN_PROGRESS[status.type]) continue; + const isInMatch = status.type === "MATCH"; + return { - type: status.type === "MATCH" ? "TOURNAMENT_MATCH" : "TOURNAMENT_PLAYING", + type: isInMatch ? "TOURNAMENT_MATCH" : "TOURNAMENT_WAITING", subtitle: tournament.ctx.name, badge: null, - matchId: status.type === "MATCH" ? status.matchId : null, + matchId: isInMatch ? status.matchId : null, tournamentId: tournament.ctx.id, + streamUrl: isInMatch + ? tournamentStreamUrl({ + tournament, + friendId, + matchId: status.matchId, + opponentId: status.opponentId, + }) + : null, }; } return null; } + +/** + * Where the friend's ongoing tournament match can be watched, preferring the view + * that shows the friend best: their own stream, then a teammate's stream, then the + * official cast of the match and finally an opponent's. + */ +function tournamentStreamUrl({ + tournament, + friendId, + matchId, + opponentId, +}: { + tournament: Tournament; + friendId: number; + matchId: number; + opponentId: number; +}) { + const streamingParticipantIds = new Set(tournament.streamingParticipantIds); + const ownTeamMembers = + tournament.teamMemberOfByUser({ id: friendId })?.members ?? []; + + const friendAccount = streamingTwitchAccount( + ownTeamMembers.filter((member) => member.userId === friendId), + streamingParticipantIds, + ); + if (friendAccount) return twitchUrl(friendAccount); + + const teammateAccount = streamingTwitchAccount( + ownTeamMembers.filter((member) => member.userId !== friendId), + streamingParticipantIds, + ); + if (teammateAccount) return twitchUrl(teammateAccount); + + const castAccount = tournament.ctx.castedMatchesInfo?.castedMatches.find( + (castedMatch) => castedMatch.matchId === matchId, + )?.twitchAccount; + if (castAccount) return twitchUrl(castAccount); + + const opponentAccount = streamingTwitchAccount( + tournament.teamById(opponentId)?.members ?? [], + streamingParticipantIds, + ); + + return opponentAccount ? twitchUrl(opponentAccount) : null; +} + +function streamingTwitchAccount( + players: Array<{ userId: number; streamTwitch: string | null }>, + streamingParticipantIds: ReadonlySet, +) { + return players.find( + (player) => + streamingParticipantIds.has(player.userId) && player.streamTwitch, + )?.streamTwitch; +} diff --git a/app/features/friends/loaders/friends.server.ts b/app/features/friends/loaders/friends.server.ts index 17be3959a..0d0ffbe5a 100644 --- a/app/features/friends/loaders/friends.server.ts +++ b/app/features/friends/loaders/friends.server.ts @@ -3,19 +3,27 @@ import { requireUser } from "~/features/auth/core/user.server"; import { userPage } from "~/utils/urls"; import * as FriendRepository from "../FriendRepository.server"; import { friendActivitySortValue } from "../friends-constants"; -import { resolveFriendActivity } from "../friends-utils.server"; +import { + resolveFriendActivity, + resolveSendouQMatchStreams, +} from "../friends-utils.server"; export type FriendsLoaderData = typeof loader; export const loader = async () => { const user = requireUser(); - const [friendsWithActivity, pendingRequests, incomingRequests] = - await Promise.all([ - FriendRepository.findByUserIdWithActivity(user.id), - FriendRepository.findPendingSentRequests(user.id), - FriendRepository.findPendingReceivedRequests(user.id), - ]); + const [ + friendsWithActivity, + pendingRequests, + incomingRequests, + streamedSendouQMatches, + ] = await Promise.all([ + FriendRepository.findByUserIdWithActivity(user.id), + FriendRepository.findPendingSentRequests(user.id), + FriendRepository.findPendingReceivedRequests(user.id), + resolveSendouQMatchStreams(), + ]); const unique = R.uniqueBy(friendsWithActivity, (f) => f.id); @@ -29,6 +37,7 @@ export const loader = async () => { tournamentName: friend.tournamentName, teamMemberCount: friend.teamMemberCount, tournamentMinTeamSize: friend.tournamentMinTeamSize, + sendouQMatchStreams: streamedSendouQMatches, }); return { @@ -47,6 +56,7 @@ export const loader = async () => { activityType: activity.type, matchId: activity.matchId, tournamentId: activity.tournamentId ?? friend.tournamentId, + streamUrl: activity.streamUrl, friendshipCreatedAt: friend.friendshipCreatedAt, }; }), @@ -64,6 +74,7 @@ export const loader = async () => { tournamentName: tm.tournamentName, teamMemberCount: tm.teamMemberCount, tournamentMinTeamSize: tm.tournamentMinTeamSize, + sendouQMatchStreams: streamedSendouQMatches, }); return { @@ -81,6 +92,7 @@ export const loader = async () => { activityType: activity.type, matchId: activity.matchId, tournamentId: activity.tournamentId ?? tm.tournamentId, + streamUrl: activity.streamUrl, }; }), [(tm) => friendActivitySortValue(tm.activityType), "desc"], diff --git a/app/features/sidebar/core/sidebar.server.ts b/app/features/sidebar/core/sidebar.server.ts index 4aa690c95..bdd82b1f9 100644 --- a/app/features/sidebar/core/sidebar.server.ts +++ b/app/features/sidebar/core/sidebar.server.ts @@ -14,11 +14,12 @@ import { import * as FriendRepository from "~/features/friends/FriendRepository.server"; import { type FriendActivityType, - isLiveFriendActivity, + isInProgressFriendActivity, } from "~/features/friends/friends-constants"; import { type FriendActivity, resolveFriendActivity, + resolveSendouQMatchStreams, } from "~/features/friends/friends-utils.server"; import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server"; import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server"; @@ -60,6 +61,7 @@ export type SidebarFriend = { activityType: FriendActivityType | null; matchId: number | null; tournamentId: number | null; + streamUrl: string | null; }; const MAX_EVENTS_VISIBLE = 5; @@ -86,12 +88,14 @@ export async function resolveSidebarData(userId: number | null) { friendsWithActivity, savedTournaments, incomingFriendRequestIds, + streamedSendouQMatches, ] = await Promise.all([ ShowcaseTournaments.categorizedTournamentsByUserId(userId), ScrimPostRepository.findUserScrims(userId), FriendRepository.findByUserIdWithActivity(userId), SavedCalendarEventRepository.findAllUpcomingByUserId(userId), FriendRepository.findPendingReceivedRequestIds(userId), + resolveSendouQMatchStreams(), ]); const seenTournamentIds = new Set(); @@ -119,7 +123,7 @@ export async function resolveSidebarData(userId: number | null) { .sort((a, b) => a.startsAt - b.startsAt) .slice(0, MAX_EVENTS_VISIBLE); - const friends = resolveFriends(friendsWithActivity); + const friends = resolveFriends(friendsWithActivity, streamedSendouQMatches); const savedTournamentIds = savedTournaments.map((t) => t.id); @@ -277,7 +281,20 @@ type FriendWithActivity = Awaited< ReturnType >[number]; -function resolveFriends(friendsWithActivity: FriendWithActivity[]) { +function resolveFriends( + friendsWithActivity: FriendWithActivity[], + streamedSendouQMatches: ReadonlyMap, +) { + const activityForRow = (row: FriendWithActivity) => + resolveFriendActivity({ + friendId: row.id, + tournamentId: row.tournamentId, + tournamentName: row.tournamentName, + teamMemberCount: row.teamMemberCount, + tournamentMinTeamSize: row.tournamentMinTeamSize, + sendouQMatchStreams: streamedSendouQMatches, + }); + const unique = R.uniqueBy(friendsWithActivity, (f) => f.id); const friendRows = unique.filter((f) => f.friendshipId !== null); const teamMemberRows = unique.filter((f) => f.friendshipId === null); @@ -297,7 +314,7 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) { const sidebarFriend = rowToSidebarFriend(friend, activity); - if (isLiveFriendActivity(activity.type)) { + if (isInProgressFriendActivity(activity.type)) { activeFriends.push(sidebarFriend); } else if (activity.type === "SENDOUQ") { sendouqFriends.push(sidebarFriend); @@ -360,16 +377,6 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) { return result.slice(0, MAX_FRIENDS_VISIBLE); } -function activityForRow(row: FriendWithActivity): FriendActivity { - return resolveFriendActivity({ - friendId: row.id, - tournamentId: row.tournamentId, - tournamentName: row.tournamentName, - teamMemberCount: row.teamMemberCount, - tournamentMinTeamSize: row.tournamentMinTeamSize, - }); -} - function rowToSidebarFriend( row: FriendWithActivity, activity: FriendActivity | null, @@ -386,6 +393,7 @@ function rowToSidebarFriend( activityType: activity?.type ?? null, matchId: activity?.matchId ?? null, tournamentId: activity?.tournamentId ?? row.tournamentId, + streamUrl: activity?.streamUrl ?? null, }; } diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts index 5c67c8678..8ad8fed8e 100644 --- a/app/features/tournament-bracket/core/Tournament.ts +++ b/app/features/tournament-bracket/core/Tournament.ts @@ -1049,6 +1049,7 @@ export class Tournament { type: "MATCH", matchId: match.id, opponent: otherTeam.name, + opponentId: otherTeam.id, } as const; } diff --git a/locales/da/friends.json b/locales/da/friends.json index bccdf5dae..b672734aa 100644 --- a/locales/da/friends.json +++ b/locales/da/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/da/q.json b/locales/da/q.json index 23ed731dc..67032d63e 100644 --- a/locales/da/q.json +++ b/locales/da/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/de/friends.json b/locales/de/friends.json index bccdf5dae..b672734aa 100644 --- a/locales/de/friends.json +++ b/locales/de/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/de/q.json b/locales/de/q.json index 4d96d5401..3dc8a4aa9 100644 --- a/locales/de/q.json +++ b/locales/de/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/en/friends.json b/locales/en/friends.json index 9c0c9f267..b606dc5d6 100644 --- a/locales/en/friends.json +++ b/locales/en/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "View tournament", "friendsList.viewMatch": "View match", "friendsList.live": "Live", + "friendsList.inMatch": "Match", + "friendsList.nextMatch": "Next", + "friendsList.watchStream": "Watch stream", "friendsList.joinSendouQ": "Join SendouQ", "friendsList.deleteFriend": "Delete friend", "friendsList.deleteConfirm": "Delete {{name}} as a friend?", diff --git a/locales/en/q.json b/locales/en/q.json index 631e31f7d..2f3caa285 100644 --- a/locales/en/q.json +++ b/locales/en/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "Loss", "match.timeline.out": "Out", "match.timeline.in": "In", - "match.timeline.live": "LIVE", + "match.timeline.ongoing": "ONGOING", "match.timeline.picked": "Picked", "match.timeline.explainer.picked": "Map picked by this team", "match.timeline.explainer.pick": "Map or mode picked", diff --git a/locales/es-ES/friends.json b/locales/es-ES/friends.json index 38898f201..fbce6b594 100644 --- a/locales/es-ES/friends.json +++ b/locales/es-ES/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/es-ES/q.json b/locales/es-ES/q.json index 4d8d679c6..d8aa0e8a2 100644 --- a/locales/es-ES/q.json +++ b/locales/es-ES/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/es-US/friends.json b/locales/es-US/friends.json index 38898f201..fbce6b594 100644 --- a/locales/es-US/friends.json +++ b/locales/es-US/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/es-US/q.json b/locales/es-US/q.json index bab31db12..81ab3834e 100644 --- a/locales/es-US/q.json +++ b/locales/es-US/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/fr-CA/friends.json b/locales/fr-CA/friends.json index 38898f201..fbce6b594 100644 --- a/locales/fr-CA/friends.json +++ b/locales/fr-CA/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/fr-CA/q.json b/locales/fr-CA/q.json index d58d9e3ae..7183f7039 100644 --- a/locales/fr-CA/q.json +++ b/locales/fr-CA/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/fr-EU/friends.json b/locales/fr-EU/friends.json index 38898f201..fbce6b594 100644 --- a/locales/fr-EU/friends.json +++ b/locales/fr-EU/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/fr-EU/q.json b/locales/fr-EU/q.json index fe5968e4a..b5bcc4216 100644 --- a/locales/fr-EU/q.json +++ b/locales/fr-EU/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/he/friends.json b/locales/he/friends.json index 781eb0c23..4dc3d11aa 100644 --- a/locales/he/friends.json +++ b/locales/he/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/he/q.json b/locales/he/q.json index 70b98ad14..9ad6cea7b 100644 --- a/locales/he/q.json +++ b/locales/he/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/it/friends.json b/locales/it/friends.json index 38898f201..fbce6b594 100644 --- a/locales/it/friends.json +++ b/locales/it/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/it/q.json b/locales/it/q.json index 8228f48cc..facb241c2 100644 --- a/locales/it/q.json +++ b/locales/it/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/ja/friends.json b/locales/ja/friends.json index 710ed7c25..94a9f4972 100644 --- a/locales/ja/friends.json +++ b/locales/ja/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/ja/q.json b/locales/ja/q.json index 3341008a9..6f7601b88 100644 --- a/locales/ja/q.json +++ b/locales/ja/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/ko/friends.json b/locales/ko/friends.json index 710ed7c25..94a9f4972 100644 --- a/locales/ko/friends.json +++ b/locales/ko/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/ko/q.json b/locales/ko/q.json index 4d96d5401..3dc8a4aa9 100644 --- a/locales/ko/q.json +++ b/locales/ko/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/nl/friends.json b/locales/nl/friends.json index bccdf5dae..b672734aa 100644 --- a/locales/nl/friends.json +++ b/locales/nl/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/nl/q.json b/locales/nl/q.json index 4d96d5401..3dc8a4aa9 100644 --- a/locales/nl/q.json +++ b/locales/nl/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/pl/friends.json b/locales/pl/friends.json index 7aae6e996..fa6b96ad3 100644 --- a/locales/pl/friends.json +++ b/locales/pl/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/pl/q.json b/locales/pl/q.json index 4d96d5401..3dc8a4aa9 100644 --- a/locales/pl/q.json +++ b/locales/pl/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/pt-BR/friends.json b/locales/pt-BR/friends.json index 38898f201..fbce6b594 100644 --- a/locales/pt-BR/friends.json +++ b/locales/pt-BR/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/pt-BR/q.json b/locales/pt-BR/q.json index 9dcece567..8d3b2f040 100644 --- a/locales/pt-BR/q.json +++ b/locales/pt-BR/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/ru/friends.json b/locales/ru/friends.json index 7aae6e996..fa6b96ad3 100644 --- a/locales/ru/friends.json +++ b/locales/ru/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "", "friendsList.viewMatch": "", "friendsList.live": "", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "", "friendsList.deleteFriend": "", "friendsList.deleteConfirm": "", diff --git a/locales/ru/q.json b/locales/ru/q.json index d6e9824c3..4f9f60b51 100644 --- a/locales/ru/q.json +++ b/locales/ru/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "", "match.timeline.out": "", "match.timeline.in": "", - "match.timeline.live": "", + "match.timeline.ongoing": "", "match.timeline.picked": "", "match.timeline.explainer.picked": "", "match.timeline.explainer.pick": "", diff --git a/locales/zh/friends.json b/locales/zh/friends.json index 61cb33325..09060c07f 100644 --- a/locales/zh/friends.json +++ b/locales/zh/friends.json @@ -10,6 +10,9 @@ "friendsList.viewTournament": "查看赛事", "friendsList.viewMatch": "查看对局", "friendsList.live": "直播中", + "friendsList.inMatch": "", + "friendsList.nextMatch": "", + "friendsList.watchStream": "", "friendsList.joinSendouQ": "加入 SendouQ", "friendsList.deleteFriend": "删除好友", "friendsList.deleteConfirm": "确定要删除好友 {{name}} 吗?", diff --git a/locales/zh/q.json b/locales/zh/q.json index 00e243b3d..b05103da4 100644 --- a/locales/zh/q.json +++ b/locales/zh/q.json @@ -174,7 +174,7 @@ "match.timeline.loss": "负", "match.timeline.out": "下场", "match.timeline.in": "上场", - "match.timeline.live": "直播中", + "match.timeline.ongoing": "", "match.timeline.picked": "已选", "match.timeline.explainer.picked": "此场地由该队伍选择", "match.timeline.explainer.pick": "已选场地或模式",