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": "已选场地或模式",