Show friends list LIVE badge only when there is a stream to watch and link to streams

This commit is contained in:
Kalle 2026-07-27 17:33:05 +03:00
parent bb374145c1
commit adce1d2099
40 changed files with 261 additions and 65 deletions

View File

@ -268,6 +268,7 @@
.listLinkSubtitleRow {
display: flex;
align-items: center;
gap: var(--s-1-5);
width: 100%;
color: var(--color-text-high);
}

View File

@ -185,7 +185,7 @@ function TimelineHeader({
) : null}
{isOngoing ? (
<span className={styles.headerScoreLive}>
{t("q:match.timeline.live")}
{t("q:match.timeline.ongoing")}
</span>
) : null}
</div>

View File

@ -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<FriendActivityBadge, string>;
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({
<ListButton
user={{ discordId, discordAvatar, customAvatarUrl }}
subtitle={subtitle}
badge={isLive ? t("friends:friendsList.live") : badge}
badgeVariant={isLive ? "warning" : "default"}
badge={
streamUrl
? t("friends:friendsList.live")
: activityBadge
? t(ACTIVITY_BADGE_TRANSLATION_KEY[activityBadge])
: badge
}
badgeVariant={streamUrl ? "warning" : "default"}
>
{name}
</ListButton>
@ -88,6 +103,17 @@ export function FriendMenu({
<SendouMenuItem href={url} icon={<User />} onAction={onNavigate}>
{t("friends:friendsList.viewUserPage")}
</SendouMenuItem>
{streamUrl ? (
<SendouMenuItem
href={streamUrl}
target="_blank"
rel="noreferrer"
icon={<TwitchIcon />}
onAction={onNavigate}
>
{t("friends:friendsList.watchStream")}
</SendouMenuItem>
) : null}
{activity?.type === "join-sendouq" ? (
<SendouMenuItem
icon={<Swords />}
@ -189,7 +215,7 @@ function resolveActivity(friend: {
}),
} as const)
: null;
case "TOURNAMENT_PLAYING":
case "TOURNAMENT_WAITING":
return friend.tournamentId
? ({
type: "view-tournament",

View File

@ -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<FriendActivityType, FriendActivityBadge | null> = {
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;
}

View File

@ -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<number, string>();
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<number, string>;
}): 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<number>,
) {
return players.find(
(player) =>
streamingParticipantIds.has(player.userId) && player.streamTwitch,
)?.streamTwitch;
}

View File

@ -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"],

View File

@ -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<number>();
@ -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<typeof FriendRepository.findByUserIdWithActivity>
>[number];
function resolveFriends(friendsWithActivity: FriendWithActivity[]) {
function resolveFriends(
friendsWithActivity: FriendWithActivity[],
streamedSendouQMatches: ReadonlyMap<number, string>,
) {
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,
};
}

View File

@ -1049,6 +1049,7 @@ export class Tournament {
type: "MATCH",
matchId: match.id,
opponent: otherTeam.name,
opponentId: otherTeam.id,
} as const;
}

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -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?",

View File

@ -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",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "",
"friendsList.viewMatch": "",
"friendsList.live": "",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",

View File

@ -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": "",

View File

@ -10,6 +10,9 @@
"friendsList.viewTournament": "查看赛事",
"friendsList.viewMatch": "查看对局",
"friendsList.live": "直播中",
"friendsList.inMatch": "",
"friendsList.nextMatch": "",
"friendsList.watchStream": "",
"friendsList.joinSendouQ": "加入 SendouQ",
"friendsList.deleteFriend": "删除好友",
"friendsList.deleteConfirm": "确定要删除好友 {{name}} 吗?",

View File

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