refactor: 收口公开详情启动意图
This commit is contained in:
@@ -3,6 +3,7 @@ import { expect, test } from 'vitest';
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||
@@ -30,12 +31,14 @@ import {
|
||||
mapWoodenFishWorkToPublicWorkDetail,
|
||||
type PlatformPublicWorkDetailKind,
|
||||
type PlatformPublicWorkDetailOpenStrategy,
|
||||
type PlatformPublicWorkStartIntentDeps,
|
||||
resolveActivePlatformPublicWorkAuthorEntry,
|
||||
resolvePlatformPublicWorkActionMode,
|
||||
resolvePlatformPublicWorkDetailOpenDecision,
|
||||
resolvePlatformPublicWorkDetailOpenStrategy,
|
||||
resolvePlatformPublicWorkLikeIntent,
|
||||
resolvePlatformPublicWorkRemixIntent,
|
||||
resolvePlatformPublicWorkStartIntent,
|
||||
resolveVisiblePuzzleDetailCoverCount,
|
||||
} from './platformPublicWorkDetailFlow';
|
||||
|
||||
@@ -56,7 +59,10 @@ type TypedPlatformPublicGalleryCardOverrides<
|
||||
function narrowTypedEntry<TSourceType extends PlatformGallerySourceType>(
|
||||
entry: TypedPlatformPublicGalleryCard,
|
||||
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
|
||||
return entry as Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }>;
|
||||
return entry as Extract<
|
||||
TypedPlatformPublicGalleryCard,
|
||||
{ sourceType: TSourceType }
|
||||
>;
|
||||
}
|
||||
|
||||
function buildRpgEntry(
|
||||
@@ -104,19 +110,47 @@ function buildTypedEntry<TSourceType extends PlatformGallerySourceType>(
|
||||
|
||||
switch (sourceType) {
|
||||
case 'puzzle':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'big-fish':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'match3d':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'square-hole':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'visual-novel':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'jump-hop':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'wooden-fish':
|
||||
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
...overrides,
|
||||
sourceType,
|
||||
});
|
||||
case 'edutainment':
|
||||
return narrowTypedEntry<TSourceType>({
|
||||
...common,
|
||||
@@ -337,6 +371,45 @@ function buildBarkBattleWork(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMatch3DWork(
|
||||
overrides: Partial<Match3DWorkSummary> = {},
|
||||
): Match3DWorkSummary {
|
||||
return {
|
||||
workId: 'match3d-work',
|
||||
profileId: 'match3d-profile',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'match3d-session',
|
||||
gameName: '抓大鹅作品',
|
||||
themeText: '经典消除',
|
||||
summary: '抓大鹅摘要',
|
||||
tags: ['抓大鹅'],
|
||||
coverImageSrc: '/match3d-cover.png',
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
publicationStatus: 'published',
|
||||
playCount: 10,
|
||||
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||
publishReady: true,
|
||||
generatedItemAssets: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildStartIntentDeps(
|
||||
overrides: Partial<PlatformPublicWorkStartIntentDeps> = {},
|
||||
): PlatformPublicWorkStartIntentDeps {
|
||||
return {
|
||||
selectedPuzzleDetail: null,
|
||||
selectedRpgDetailEntry: null,
|
||||
barkBattleGalleryEntries: [],
|
||||
barkBattleWorks: [],
|
||||
mapMatch3DWork: () => buildMatch3DWork(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('platform public work detail flow resolves detail kind for every play kind', () => {
|
||||
const cases: Array<
|
||||
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
|
||||
@@ -579,18 +652,16 @@ test('platform public work detail flow maps detail entries back to work summarie
|
||||
});
|
||||
|
||||
expect(
|
||||
mapBarkBattlePublicDetailToWorkSummary(
|
||||
{
|
||||
...buildTypedEntry('bark-battle', {
|
||||
themeTags: ['森林', '小狗', '对手'],
|
||||
coverImageSrc: '/bark-bg.png',
|
||||
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
|
||||
playCount: 11,
|
||||
recentPlayCount7d: 5,
|
||||
}),
|
||||
sourceSessionId: 'bark-draft',
|
||||
},
|
||||
),
|
||||
mapBarkBattlePublicDetailToWorkSummary({
|
||||
...buildTypedEntry('bark-battle', {
|
||||
themeTags: ['森林', '小狗', '对手'],
|
||||
coverImageSrc: '/bark-bg.png',
|
||||
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
|
||||
playCount: 11,
|
||||
recentPlayCount7d: 5,
|
||||
}),
|
||||
sourceSessionId: 'bark-draft',
|
||||
}),
|
||||
).toMatchObject({
|
||||
workId: 'bark-battle-work',
|
||||
draftId: 'bark-draft',
|
||||
@@ -605,8 +676,12 @@ test('platform public work detail flow maps detail entries back to work summarie
|
||||
recentPlayCount7d: 5,
|
||||
});
|
||||
|
||||
expect(mapPublicWorkDetailToPuzzleWork(buildTypedEntry('big-fish'))).toBeNull();
|
||||
expect(mapPublicWorkDetailToBigFishWork(buildTypedEntry('puzzle'))).toBeNull();
|
||||
expect(
|
||||
mapPublicWorkDetailToPuzzleWork(buildTypedEntry('big-fish')),
|
||||
).toBeNull();
|
||||
expect(
|
||||
mapPublicWorkDetailToBigFishWork(buildTypedEntry('puzzle')),
|
||||
).toBeNull();
|
||||
expect(
|
||||
mapPublicWorkDetailToSquareHoleWork(buildTypedEntry('puzzle')),
|
||||
).toBeNull();
|
||||
@@ -654,13 +729,15 @@ test('platform public work detail flow resolves edit mode only for owned works',
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves like intent', () => {
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('big-fish'))).toEqual(
|
||||
{
|
||||
type: 'like-big-fish',
|
||||
profileId: 'big-fish-profile',
|
||||
},
|
||||
);
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('big-fish')),
|
||||
).toEqual({
|
||||
type: 'like-big-fish',
|
||||
profileId: 'big-fish-profile',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle')),
|
||||
).toEqual({
|
||||
type: 'like-puzzle',
|
||||
profileId: 'puzzle-profile',
|
||||
});
|
||||
@@ -669,42 +746,50 @@ test('platform public work detail flow resolves like intent', () => {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'rpg-profile',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('match3d'))).toEqual(
|
||||
{
|
||||
type: 'like-rpg-gallery',
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'match3d-profile',
|
||||
},
|
||||
);
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('edutainment'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('match3d')),
|
||||
).toEqual({
|
||||
type: 'like-rpg-gallery',
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'match3d-profile',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('edutainment')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '宝贝识物点赞将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('bark-battle'))).toEqual(
|
||||
{
|
||||
type: 'unsupported',
|
||||
errorMessage: '汪汪声浪点赞将在后续版本开放。',
|
||||
},
|
||||
);
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('square-hole'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('bark-battle')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '汪汪声浪点赞将在后续版本开放。',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('square-hole')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '方洞挑战点赞将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkLikeIntent(buildTypedEntry('visual-novel'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('visual-novel')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '视觉小说点赞将在后续版本开放。',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves remix intent', () => {
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('big-fish'))).toEqual(
|
||||
{
|
||||
type: 'remix-big-fish',
|
||||
profileId: 'big-fish-profile',
|
||||
selectionStage: 'big-fish-result',
|
||||
},
|
||||
);
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('puzzle'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('big-fish')),
|
||||
).toEqual({
|
||||
type: 'remix-big-fish',
|
||||
profileId: 'big-fish-profile',
|
||||
selectionStage: 'big-fish-result',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('puzzle')),
|
||||
).toEqual({
|
||||
type: 'remix-puzzle',
|
||||
profileId: 'puzzle-profile',
|
||||
selectionStage: 'puzzle-result',
|
||||
@@ -714,38 +799,236 @@ test('platform public work detail flow resolves remix intent', () => {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'rpg-profile',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('match3d'))).toEqual(
|
||||
{
|
||||
type: 'unsupported',
|
||||
errorMessage: '抓大鹅作品改造将在后续版本开放。',
|
||||
},
|
||||
);
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('square-hole'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('match3d')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '抓大鹅作品改造将在后续版本开放。',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('square-hole')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '方洞挑战作品改造将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('jump-hop'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('jump-hop')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '跳一跳作品改造将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('wooden-fish'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('wooden-fish')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '敲木鱼作品改造将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('visual-novel'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('visual-novel')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '视觉小说作品改造将在后续版本开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('edutainment'))).toEqual({
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('edutainment')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '宝贝识物作品改造将在创作链路接入后开放。',
|
||||
});
|
||||
expect(resolvePlatformPublicWorkRemixIntent(buildTypedEntry('bark-battle'))).toEqual(
|
||||
{
|
||||
type: 'unsupported',
|
||||
errorMessage: '汪汪声浪作品改造将在后续版本开放。',
|
||||
},
|
||||
);
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('bark-battle')),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '汪汪声浪作品改造将在后续版本开放。',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves start intent for direct launches', () => {
|
||||
const bigFishEntry = buildTypedEntry('big-fish');
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(bigFishEntry, buildStartIntentDeps()),
|
||||
).toEqual({
|
||||
type: 'start-big-fish',
|
||||
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
|
||||
const selectedPuzzleDetail = buildPuzzleWork({
|
||||
profileId: 'puzzle-profile',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
buildTypedEntry('puzzle'),
|
||||
buildStartIntentDeps({ selectedPuzzleDetail }),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-puzzle',
|
||||
work: selectedPuzzleDetail,
|
||||
returnStage: 'work-detail',
|
||||
authMode: 'isolated',
|
||||
});
|
||||
|
||||
const puzzleEntry = buildTypedEntry('puzzle', {
|
||||
profileId: 'fallback-puzzle-profile',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
puzzleEntry,
|
||||
buildStartIntentDeps({
|
||||
selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }),
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-puzzle',
|
||||
work: mapPublicWorkDetailToPuzzleWork(puzzleEntry),
|
||||
returnStage: 'work-detail',
|
||||
authMode: 'isolated',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
buildTypedEntry('jump-hop'),
|
||||
buildStartIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-jump-hop',
|
||||
profileId: 'jump-hop-profile',
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
buildTypedEntry('wooden-fish'),
|
||||
buildStartIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-wooden-fish',
|
||||
profileId: 'wooden-fish-profile',
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
buildTypedEntry('visual-novel'),
|
||||
buildStartIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-visual-novel',
|
||||
profileId: 'visual-novel-profile',
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
buildTypedEntry('edutainment'),
|
||||
buildStartIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-edutainment',
|
||||
entry: buildTypedEntry('edutainment'),
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves start intent for mapper-backed launches', () => {
|
||||
const match3DEntry = buildTypedEntry('match3d');
|
||||
const match3DWork = buildMatch3DWork({ workId: 'mapped-match3d-work' });
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
match3DEntry,
|
||||
buildStartIntentDeps({
|
||||
mapMatch3DWork: (entry) =>
|
||||
entry === match3DEntry ? match3DWork : null,
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-match3d',
|
||||
work: match3DWork,
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
match3DEntry,
|
||||
buildStartIntentDeps({ mapMatch3DWork: () => null }),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'blocked',
|
||||
errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。',
|
||||
});
|
||||
|
||||
const squareHoleEntry = buildTypedEntry('square-hole');
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
squareHoleEntry,
|
||||
buildStartIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-square-hole',
|
||||
work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry),
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves bark battle start work priority', () => {
|
||||
const entry = buildTypedEntry('bark-battle');
|
||||
const galleryWork = buildBarkBattleWork({
|
||||
workId: 'bark-battle-work',
|
||||
title: '作品架缓存',
|
||||
});
|
||||
const loadedWork = buildBarkBattleWork({
|
||||
workId: 'bark-battle-work',
|
||||
title: '完整作品列表',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
entry,
|
||||
buildStartIntentDeps({
|
||||
barkBattleGalleryEntries: [galleryWork],
|
||||
barkBattleWorks: [loadedWork],
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-bark-battle',
|
||||
work: galleryWork,
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
entry,
|
||||
buildStartIntentDeps({
|
||||
barkBattleWorks: [loadedWork],
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'start-bark-battle',
|
||||
work: loadedWork,
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(entry, buildStartIntentDeps()),
|
||||
).toEqual({
|
||||
type: 'start-bark-battle',
|
||||
work: mapBarkBattlePublicDetailToWorkSummary(entry),
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves rpg start intent from loaded detail', () => {
|
||||
const rpgEntry = buildRpgEntry();
|
||||
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(
|
||||
rpgEntry,
|
||||
buildStartIntentDeps({ selectedRpgDetailEntry: rpgEntry }),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'record-rpg-gallery-play',
|
||||
entry: rpgEntry,
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkStartIntent(rpgEntry, buildStartIntentDeps()),
|
||||
).toEqual({
|
||||
type: 'blocked',
|
||||
errorMessage: '作品详情尚未读取完成。',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves direct open decision', () => {
|
||||
|
||||
Reference in New Issue
Block a user