QUEST · CAMPAIGN 화면 — 퀘스트 참여·생성·인증 · 캠페인 목록·상세·후원·랭킹
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
/// 캠페인이 그리는 값의 형태 — 화면 대조 원장 F5.
|
||||
///
|
||||
/// 근거는 Figma 명세 구간이다.
|
||||
/// - 목록 `523:18347`·`523:18388`·`523:18399` · 검색 `523:18417`~`523:18429`
|
||||
/// - 상세 `523:18487`~`523:18589` · 기여 바텀시트 `527:21100`
|
||||
/// - 랭킹 `536:14345`~`536:14382` · 댓글 `527:21192`·`527:21198`
|
||||
library;
|
||||
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../my/my_data.dart';
|
||||
|
||||
/// 목록 정렬. 명세 — *디폴트 최신순 선택 · 단일 선택*.
|
||||
enum CampaignSort {
|
||||
latest,
|
||||
popular,
|
||||
contributing;
|
||||
|
||||
String get label => switch (this) {
|
||||
latest => appL10n.sortLatest,
|
||||
popular => appL10n.sortPopular,
|
||||
contributing => appL10n.campaignSortContributing,
|
||||
};
|
||||
}
|
||||
|
||||
/// 캠페인 진행 상태. 정책 설계서 §P-09 의 상태 전이를 따른다.
|
||||
enum CampaignStatus {
|
||||
/// 모금 중.
|
||||
funding,
|
||||
|
||||
/// 목표 달성 후 집행.
|
||||
success,
|
||||
|
||||
/// 목표 미달 — 100% 자동 환불 대상.
|
||||
failed,
|
||||
|
||||
/// 결과 보고까지 끝남.
|
||||
reported,
|
||||
|
||||
/// 공개 전 — 초안 · 운영 검수 대기. 개설자에게만 보인다.
|
||||
review,
|
||||
|
||||
/// 운영 반려 — 등록비 반환(B-58).
|
||||
rejected,
|
||||
|
||||
/// 개설자 철회(B-58). 공개 전 철회만 등록비를 돌려받는다.
|
||||
withdrawn,
|
||||
|
||||
/// 운영 취소(회사귀책, B-58) — 등록비 · 후원 전액 반환.
|
||||
cancelled;
|
||||
|
||||
/// 공개 모금 흐름 밖의 상태 안내. 모금 · 결과 상태는 null.
|
||||
String? get notice => switch (this) {
|
||||
review => appL10n.campaignStatusReview,
|
||||
rejected => appL10n.campaignStatusRejected,
|
||||
withdrawn => appL10n.campaignStatusWithdrawn,
|
||||
cancelled => appL10n.campaignStatusCancelled,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// 목록 카드 하나.
|
||||
///
|
||||
/// 명세 `523:18399` — *대표 이미지 · 작성자 · 작성일시 · 달성률(%) · 제목 ·*
|
||||
/// *내용 · 좋아요 · 댓글 · 조회수 · 공유 수*
|
||||
class CampaignCardData {
|
||||
const CampaignCardData({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.authorName,
|
||||
this.summary = '',
|
||||
this.fandomId = '',
|
||||
this.fandomName = '',
|
||||
required this.createdAt,
|
||||
required this.status,
|
||||
required this.goalPop,
|
||||
required this.raisedPop,
|
||||
required this.supporters,
|
||||
required this.likes,
|
||||
required this.comments,
|
||||
required this.views,
|
||||
required this.shares,
|
||||
this.mine = false,
|
||||
this.contributedPop = 0,
|
||||
this.creatorUid,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
/// 개설자 UID — 캠페인 신고 · 개설자 차단의 대상. 없으면 차단할 수 없다.
|
||||
final String? creatorUid;
|
||||
final String title;
|
||||
|
||||
/// 한 줄 소개 — 서버 문서의 `summary`. 공유 문구가 이 값을 쓴다.
|
||||
final String summary;
|
||||
|
||||
final String body;
|
||||
final String authorName;
|
||||
|
||||
/// 소속 팬덤 — 서버 문서의 `fandomId`.
|
||||
final String fandomId;
|
||||
|
||||
/// 행성 이름.
|
||||
///
|
||||
/// **캠페인 문서의 필드가 아니다.** `firestore.rules` 의 `campaigns` 생성 규칙이
|
||||
/// `keys().hasOnly([...])` 로 스키마를 닫아 두었고 그 목록에 `fandomName` 이
|
||||
/// 없다. 비정규화 필드를 넣으면 캠페인 생성 자체가 거부된다. 그래서 이 값은
|
||||
/// `fandoms/{fandomId}.name` 을 읽어 채우는 **파생값**이고, 해석은 캠페인 서버
|
||||
/// 연결 회차가 맡는다. 비어 있으면 공유 문구에서 행성 줄과 팬덤 해시태그가 함께
|
||||
/// 빠진다(정책 원장 B-78 — 고정 3종이 아니다).
|
||||
final String fandomName;
|
||||
final DateTime createdAt;
|
||||
final CampaignStatus status;
|
||||
|
||||
final int goalPop;
|
||||
final int raisedPop;
|
||||
final int supporters;
|
||||
|
||||
final int likes;
|
||||
final int comments;
|
||||
final int views;
|
||||
final int shares;
|
||||
|
||||
/// 내가 만든 캠페인인가. 명세 — *기여하기 버튼은 타인 캠페인에서만 표시*.
|
||||
final bool mine;
|
||||
|
||||
/// 내가 이미 기여한 POP. 「기여중」 필터의 기준이다.
|
||||
final int contributedPop;
|
||||
|
||||
/// 달성률(%). 목표가 0이면 0.
|
||||
int get achievedPercent =>
|
||||
goalPop == 0 ? 0 : ((raisedPop / goalPop) * 100).round();
|
||||
|
||||
/// 인기순 정렬 키. 명세 — *좋아요 > 댓글 > 조회수 > 공유수*.
|
||||
/// 앞의 값이 같을 때만 뒤를 본다는 뜻이라 순서대로 비교한다.
|
||||
int comparePopularity(CampaignCardData other) {
|
||||
for (final pick in <int Function(CampaignCardData)>[
|
||||
(c) => c.likes,
|
||||
(c) => c.comments,
|
||||
(c) => c.views,
|
||||
(c) => c.shares,
|
||||
]) {
|
||||
final d = pick(other).compareTo(pick(this));
|
||||
if (d != 0) return d;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 기여 랭킹 한 줄.
|
||||
class ContributorRank {
|
||||
const ContributorRank({
|
||||
required this.rank,
|
||||
required this.name,
|
||||
required this.pop,
|
||||
this.change = 0,
|
||||
this.isMe = false,
|
||||
});
|
||||
|
||||
final int rank;
|
||||
final String name;
|
||||
final int pop;
|
||||
|
||||
/// 순위 변동값. 양수면 올라갔다.
|
||||
final int change;
|
||||
final bool isMe;
|
||||
}
|
||||
|
||||
/// 랭킹 집계 구간. 명세 `536:14382` — *실시간 · 일간 · 주간*.
|
||||
enum RankWindow {
|
||||
live,
|
||||
daily,
|
||||
weekly;
|
||||
|
||||
String get label => switch (this) {
|
||||
live => appL10n.rankScopeLive,
|
||||
daily => appL10n.rankWindowDaily,
|
||||
weekly => appL10n.rankWindowWeekly,
|
||||
};
|
||||
}
|
||||
|
||||
/// 응원 게시판 댓글 한 장의 크기. 이만큼 오면 「더 보기」가 붙는다.
|
||||
const campaignCommentPageSize = 20;
|
||||
|
||||
/// 응원 게시판 댓글 — 서버 `campaigns/{id}/comments/{cid}` .
|
||||
class CampaignComment {
|
||||
const CampaignComment({
|
||||
required this.id,
|
||||
required this.authorName,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
this.authorUid,
|
||||
this.isMine = false,
|
||||
this.likes = 0,
|
||||
this.liked = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
/// 공개 닉네임(없으면 「팬」). 공급자 실명은 쓰지 않는다(M-7).
|
||||
final String authorName;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
|
||||
/// 차단 대상. 없으면 차단할 수 없다.
|
||||
final String? authorUid;
|
||||
final bool isMine;
|
||||
|
||||
/// 좋아요 수(서버 트리거가 센 `likeCount`)와 내가 눌렀는가.
|
||||
final int likes;
|
||||
final bool liked;
|
||||
}
|
||||
|
||||
/// 상세 화면이 필요로 하는 것.
|
||||
class CampaignDetail {
|
||||
const CampaignDetail({
|
||||
required this.card,
|
||||
required this.mediaLabels,
|
||||
required this.endsAt,
|
||||
required this.topContributors,
|
||||
this.liked = false,
|
||||
this.report,
|
||||
});
|
||||
|
||||
final CampaignCardData card;
|
||||
|
||||
/// 결과보고. 게시 전이면 null.
|
||||
final CampaignReport? report;
|
||||
|
||||
/// 내가 이미 좋아요를 눌렀는가. 서버에 저장된 상태에서 시작해야 두 번째 탭이 취소가 된다.
|
||||
final bool liked;
|
||||
|
||||
/// 사진·영상. 자산이 아직 없어 라벨로 둔다. 개수가 인디케이터 수를 정한다.
|
||||
final List<String> mediaLabels;
|
||||
|
||||
final DateTime endsAt;
|
||||
final List<ContributorRank> topContributors;
|
||||
|
||||
/// 남은 시간. 지났으면 [Duration.zero].
|
||||
Duration remaining({DateTime? now}) {
|
||||
final gap = endsAt.difference(now ?? DateTime.now());
|
||||
return gap.isNegative ? Duration.zero : gap;
|
||||
}
|
||||
}
|
||||
|
||||
/// 결과보고 — 요구사항 FR-CMP-06 「집행 사진 · 영수증 · 예산 사용 내역 공개」.
|
||||
/// 서버 문서 `campaigns/{id}/reports/main`. 올리는 쪽은 운영이다(원장 B-35).
|
||||
class CampaignReport {
|
||||
const CampaignReport({
|
||||
required this.body,
|
||||
required this.photoUrls,
|
||||
required this.receiptUrls,
|
||||
required this.budget,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String body;
|
||||
final List<String> photoUrls;
|
||||
final List<String> receiptUrls;
|
||||
final List<({String label, int amount})> budget;
|
||||
final DateTime createdAt;
|
||||
|
||||
int get budgetTotal => budget.fold(0, (sum, l) => sum + l.amount);
|
||||
}
|
||||
|
||||
/// 사용자 캠페인 등록비 — 원장 B-57(🟡 운영값). 서버 `fn-campaign-create` 가 실제로 뺀다.
|
||||
const int campaignRegistrationFeePop = 100;
|
||||
|
||||
/// Campaign Product Catalog 상품 하나 — 원장 B-59(상품 · 필요 기여 POP) · C-14(권장 모금 기간).
|
||||
class CampaignProduct {
|
||||
const CampaignProduct(this.name, this.goalPop, this.recommendedDays);
|
||||
|
||||
final String name;
|
||||
|
||||
/// 필요 기여 POP. 목표액은 이 값 그대로다(B-32 — 사용자 임의 입력 없음).
|
||||
final int goalPop;
|
||||
|
||||
/// 권장 모금 기간(일). 날짜 선택에 자동으로 들어간다(C-14).
|
||||
final int recommendedDays;
|
||||
}
|
||||
|
||||
/// MOCK: Catalog 가 앱에 아직 연결되지 않아 사본(`287:16991`) 칩 문구와 표본 수치를 쓴다.
|
||||
/// 인스타 광고 300,000 만 사본 값이고 나머지 POP · 일수는 S03 제안표 확정 전 자리표시다.
|
||||
const sampleCampaignCatalog = [
|
||||
CampaignProduct('인스타 광고', 300000, 14),
|
||||
CampaignProduct('지하철 CM보드', 800000, 30),
|
||||
CampaignProduct('커피차 서포트', 500000, 14),
|
||||
CampaignProduct('커스텀 인이어', 200000, 21),
|
||||
];
|
||||
|
||||
/// 새 캠페인이 만드는 값.
|
||||
class CampaignDraft {
|
||||
const CampaignDraft({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.goalPop,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final int goalPop;
|
||||
final int days;
|
||||
}
|
||||
|
||||
abstract interface class CampaignRepository {
|
||||
/// 명세 — *기본 10개 노출 · 아래 스크롤 시 10개 업데이트*.
|
||||
Future<List<CampaignCardData>> load({
|
||||
CampaignSort sort = CampaignSort.latest,
|
||||
int limit = 10,
|
||||
});
|
||||
|
||||
Future<List<CampaignCardData>> more({required int offset, int limit = 10});
|
||||
|
||||
Future<CampaignDetail> detail(String campaignId);
|
||||
|
||||
Future<List<ContributorRank>> ranking(
|
||||
String campaignId, {
|
||||
RankWindow window = RankWindow.live,
|
||||
});
|
||||
|
||||
Future<List<String>> recentQueries();
|
||||
|
||||
Future<void> clearQueries();
|
||||
|
||||
/// 최근 검색어 한 개 지우기 — 시안 `272:241681` 검색어 칩의 ✕.
|
||||
Future<void> removeQuery(String query);
|
||||
|
||||
Future<List<CampaignCardData>> search(String query);
|
||||
|
||||
/// 기여. **잔액은 서버가 깎는다** — `fn-campaign-fund` 가 원장을 지난다.
|
||||
/// 실패하면 던진다 — 화면은 성공으로 그리지 않는다.
|
||||
Future<int> contribute(String campaignId, int amountPop);
|
||||
|
||||
/// 후원에 쓸 수 있는 잔액 — 기여 POP(정책 원장 B-33).
|
||||
Future<int> balance();
|
||||
|
||||
/// 등록비에 쓸 수 있는 잔액 — 일반 + 기여 POP. 등록비는 겸용이다(B-57 · B-06).
|
||||
Future<int> feeBalance();
|
||||
|
||||
/// 좋아요 저장. 실패하면 던진다.
|
||||
Future<void> like(String campaignId, bool on);
|
||||
|
||||
Future<String> create(CampaignDraft draft);
|
||||
|
||||
/// 내가 만든 캠페인 — 공개 전 · 철회 · 취소 상태까지(MY/내 캠페인).
|
||||
Future<List<CampaignCardData>> mine();
|
||||
|
||||
/// 개설자 철회(`fn-campaign-withdraw` · B-58). 실패하면 던진다.
|
||||
Future<void> withdraw(String campaignId);
|
||||
|
||||
/// 응원 게시판 댓글 한 장 — 최신순(명세 `527:21192`). [more] 가 false 면 첫
|
||||
/// 장부터 다시 읽는다. 차단한 작성자의 댓글은 뺀다.
|
||||
Future<List<CampaignComment>> comments(
|
||||
String campaignId, {
|
||||
bool more = false,
|
||||
});
|
||||
|
||||
/// 댓글 저장. 실패하면 던진다.
|
||||
Future<CampaignComment> addComment(String campaignId, String body);
|
||||
|
||||
Future<void> deleteComment(String campaignId, String commentId);
|
||||
|
||||
/// 신고 — [reason] 은 신고 화면의 사유 문구(`reportReasonCodes` 의 키).
|
||||
Future<void> reportComment(
|
||||
String campaignId,
|
||||
CampaignComment comment,
|
||||
String reason,
|
||||
);
|
||||
|
||||
/// 작성자 차단 — 이후 그 작성자의 댓글은 목록에서 빠진다.
|
||||
Future<void> blockAuthor(CampaignComment comment);
|
||||
|
||||
/// 댓글 좋아요 저장 — 1인 1회. 실패하면 던진다.
|
||||
Future<void> likeComment(String campaignId, String commentId, bool on);
|
||||
|
||||
/// 캠페인 신고 — [reason] 은 신고 화면의 사유 문구. 같은 사람의 재신고는 던진다.
|
||||
Future<void> reportCampaign(String campaignId, String reason);
|
||||
|
||||
/// 개설자 차단 — 이후 그 개설자의 캠페인 · 댓글이 목록에서 빠진다.
|
||||
Future<void> blockCreator(CampaignCardData card);
|
||||
|
||||
/// 댓글 작성 자격 — 공개 닉네임이 있는가(비었거나 이메일 형태면 없음). 규칙도
|
||||
/// 응원 게시판 댓글에는 「팬」 대체값을 받지 않는다.
|
||||
Future<bool> hasPublicNickname();
|
||||
|
||||
/// 닉네임을 정하러 갈 MY 저장소(닉네임 수정 화면이 쓴다).
|
||||
MyRepository get my;
|
||||
}
|
||||
|
||||
/// 서버가 붙기 전까지 쓰는 표본. **정책값이 아니다.**
|
||||
class SampleCampaignRepository implements CampaignRepository {
|
||||
SampleCampaignRepository();
|
||||
|
||||
static final _base = DateTime(2026, 9, 1, 12);
|
||||
|
||||
final List<String> _queries = ['강남역', 'LED', '커피차'];
|
||||
|
||||
static final List<CampaignCardData> _cards = [
|
||||
CampaignCardData(
|
||||
id: 'c1',
|
||||
summary: '도심 한가운데에서 함께 축하해요.',
|
||||
fandomId: 'bts',
|
||||
fandomName: 'BTS',
|
||||
title: '강남역 초대형 LED 광고',
|
||||
body: 'BTS 데뷔 기념 팬 연합의 초대형 프로젝트입니다. 도심을 BTS의 색으로 물들여주세요.',
|
||||
authorName: '다이브짱',
|
||||
createdAt: _base.subtract(const Duration(hours: 3)),
|
||||
status: CampaignStatus.funding,
|
||||
goalPop: 100000,
|
||||
raisedPop: 92000,
|
||||
supporters: 48,
|
||||
likes: 820,
|
||||
comments: 24,
|
||||
views: 12400,
|
||||
shares: 96,
|
||||
contributedPop: 500,
|
||||
),
|
||||
CampaignCardData(
|
||||
id: 'c2',
|
||||
summary: '무대 뒤에서 고생하는 분들께 시원한 한 잔.',
|
||||
fandomId: 'bts',
|
||||
fandomName: 'BTS',
|
||||
title: '뮤직뱅크 커피차 보내기',
|
||||
body: '무더위에 고생하는 스태프와 아티스트를 위해 커피차를 준비합니다.',
|
||||
authorName: '아미보이',
|
||||
createdAt: _base.subtract(const Duration(days: 2)),
|
||||
status: CampaignStatus.funding,
|
||||
goalPop: 50000,
|
||||
raisedPop: 12000,
|
||||
supporters: 21,
|
||||
likes: 340,
|
||||
comments: 12,
|
||||
views: 5200,
|
||||
shares: 18,
|
||||
),
|
||||
CampaignCardData(
|
||||
id: 'c3',
|
||||
summary: '데뷔일을 2호선 전 역에서.',
|
||||
fandomId: 'newjeans',
|
||||
fandomName: 'NewJeans',
|
||||
title: '지하철 생일 광고',
|
||||
body: '데뷔일에 맞춰 2호선 전 역사에 생일 광고를 겁니다.',
|
||||
authorName: '네누',
|
||||
createdAt: _base.subtract(const Duration(days: 12)),
|
||||
status: CampaignStatus.success,
|
||||
goalPop: 30000,
|
||||
raisedPop: 30000,
|
||||
supporters: 64,
|
||||
likes: 1240,
|
||||
comments: 51,
|
||||
views: 21000,
|
||||
shares: 210,
|
||||
mine: true,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<CampaignCardData>> load({
|
||||
CampaignSort sort = CampaignSort.latest,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
final list = [..._cards];
|
||||
switch (sort) {
|
||||
case CampaignSort.latest:
|
||||
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
case CampaignSort.popular:
|
||||
list.sort((a, b) => a.comparePopularity(b));
|
||||
case CampaignSort.contributing:
|
||||
list.retainWhere((c) => c.contributedPop > 0);
|
||||
}
|
||||
return list.take(limit).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CampaignCardData>> more({
|
||||
required int offset,
|
||||
int limit = 10,
|
||||
}) async => _cards.skip(offset).take(limit).toList();
|
||||
|
||||
@override
|
||||
Future<CampaignDetail> detail(String campaignId) async {
|
||||
final card = _cards.firstWhere(
|
||||
(c) => c.id == campaignId,
|
||||
orElse: () => _cards.first,
|
||||
);
|
||||
return CampaignDetail(
|
||||
card: card,
|
||||
mediaLabels: const ['대표 이미지', '조감도', '위치'],
|
||||
endsAt: _base.add(const Duration(days: 3)),
|
||||
topContributors: const [
|
||||
ContributorRank(rank: 1, name: '버니즈', pop: 12000, change: 2),
|
||||
ContributorRank(rank: 2, name: '캐럿', pop: 9800, change: -1),
|
||||
ContributorRank(rank: 3, name: '나', pop: 500, change: 4, isMe: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ContributorRank>> ranking(
|
||||
String campaignId, {
|
||||
RankWindow window = RankWindow.live,
|
||||
}) async => const [
|
||||
ContributorRank(rank: 1, name: '버니즈', pop: 12000, change: 2),
|
||||
ContributorRank(rank: 2, name: '캐럿', pop: 9800, change: -1),
|
||||
ContributorRank(rank: 3, name: '나', pop: 500, change: 4, isMe: true),
|
||||
ContributorRank(rank: 4, name: '무민', pop: 400),
|
||||
ContributorRank(rank: 5, name: '보라', pop: 320),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<String>> recentQueries() async => [..._queries];
|
||||
|
||||
@override
|
||||
Future<void> clearQueries() async => _queries.clear();
|
||||
|
||||
@override
|
||||
Future<void> removeQuery(String query) async => _queries.remove(query);
|
||||
|
||||
@override
|
||||
Future<List<CampaignCardData>> search(String query) async {
|
||||
// 검색어를 최신순 · 중복 제거로 앞에 쌓는다(명세 `523:18429`).
|
||||
_queries
|
||||
..remove(query)
|
||||
..insert(0, query);
|
||||
return _cards
|
||||
.where((c) => c.title.contains(query) || c.body.contains(query))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> contribute(String campaignId, int amountPop) async => amountPop;
|
||||
|
||||
@override
|
||||
Future<int> balance() async => 12540;
|
||||
|
||||
@override
|
||||
Future<int> feeBalance() => balance();
|
||||
|
||||
@override
|
||||
Future<void> like(String campaignId, bool on) async {}
|
||||
|
||||
@override
|
||||
Future<String> create(CampaignDraft draft) async => 'c-new';
|
||||
|
||||
@override
|
||||
Future<List<CampaignCardData>> mine() async =>
|
||||
(await load(limit: 100)).where((c) => c.mine).toList();
|
||||
|
||||
@override
|
||||
Future<void> withdraw(String campaignId) async {}
|
||||
|
||||
late final List<CampaignComment> _comments = [
|
||||
CampaignComment(
|
||||
id: 'm1',
|
||||
authorName: '아미보이',
|
||||
authorUid: 'u-army',
|
||||
body: '꼭 성공했으면 좋겠어요!',
|
||||
createdAt: _base.subtract(const Duration(minutes: 20)),
|
||||
),
|
||||
CampaignComment(
|
||||
id: 'm2',
|
||||
authorName: '다이브짱',
|
||||
authorUid: 'u-dive',
|
||||
body: '조감도 너무 예쁘네요',
|
||||
createdAt: _base.subtract(const Duration(hours: 5)),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<CampaignComment>> comments(
|
||||
String campaignId, {
|
||||
bool more = false,
|
||||
}) async => more ? const [] : [..._comments];
|
||||
|
||||
@override
|
||||
Future<CampaignComment> addComment(String campaignId, String body) async {
|
||||
final c = CampaignComment(
|
||||
id: 'm${_comments.length + 1}',
|
||||
authorName: appL10n.commonMe,
|
||||
body: body,
|
||||
createdAt: DateTime.now(),
|
||||
isMine: true,
|
||||
);
|
||||
_comments.insert(0, c);
|
||||
return c;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteComment(String campaignId, String commentId) async =>
|
||||
_comments.removeWhere((c) => c.id == commentId);
|
||||
|
||||
@override
|
||||
Future<void> reportComment(
|
||||
String campaignId,
|
||||
CampaignComment comment,
|
||||
String reason,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> blockAuthor(CampaignComment comment) async =>
|
||||
_comments.removeWhere((c) => c.authorUid == comment.authorUid);
|
||||
|
||||
@override
|
||||
Future<void> likeComment(
|
||||
String campaignId,
|
||||
String commentId,
|
||||
bool on,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> reportCampaign(String campaignId, String reason) async {}
|
||||
|
||||
@override
|
||||
Future<void> blockCreator(CampaignCardData card) async {}
|
||||
|
||||
@override
|
||||
Future<bool> hasPublicNickname() async => true;
|
||||
|
||||
@override
|
||||
late final MyRepository my = SampleMyRepository();
|
||||
}
|
||||
Reference in New Issue
Block a user