퀘스트 완료 햅틱 · HOME 행성 반응 · Push·라이브 띠 중복 제어 · 알림함 위계 · Push 탭 화면 이동
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart' show FirebaseMessaging;
|
||||
import 'package:firebase_messaging/firebase_messaging.dart'
|
||||
show FirebaseMessaging;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../data/backend.dart';
|
||||
@@ -134,10 +135,12 @@ class PushMessaging {
|
||||
required void Function(PushNotice notice) onForeground,
|
||||
required void Function(PushNotice notice) onOpened,
|
||||
}) async {
|
||||
_foreground = FirebaseMessaging.onMessage
|
||||
.listen((m) => onForeground(PushNotice.of(m)));
|
||||
_opened = FirebaseMessaging.onMessageOpenedApp
|
||||
.listen((m) => onOpened(PushNotice.of(m)));
|
||||
_foreground = FirebaseMessaging.onMessage.listen(
|
||||
(m) => onForeground(PushNotice.of(m)),
|
||||
);
|
||||
_opened = FirebaseMessaging.onMessageOpenedApp.listen(
|
||||
(m) => onOpened(PushNotice.of(m)),
|
||||
);
|
||||
_tokenRefresh = _transport.onTokenRefresh.listen(_saveToken);
|
||||
|
||||
final initial = await _transport.getInitialMessage();
|
||||
@@ -160,9 +163,7 @@ class PushMessaging {
|
||||
Future<void> clearOnSignOut() async {
|
||||
late final Future<void> run;
|
||||
run = _unsubscribeAll()
|
||||
.catchError(
|
||||
(Object e) => debugPrint('[Push] topic cleanup failed: $e'),
|
||||
)
|
||||
.catchError((Object e) => debugPrint('[Push] topic cleanup failed: $e'))
|
||||
.whenComplete(() {
|
||||
if (identical(_pendingClear, run)) _pendingClear = null;
|
||||
});
|
||||
@@ -265,58 +266,116 @@ class PushNotice {
|
||||
required this.body,
|
||||
required this.kind,
|
||||
this.fandomId,
|
||||
this.phase = '',
|
||||
this.targetPath,
|
||||
this.questId,
|
||||
this.campaignId,
|
||||
});
|
||||
|
||||
factory PushNotice.of(RemoteMessage message) => PushNotice(
|
||||
title: message.notification?.title ?? '',
|
||||
body: message.notification?.body ?? '',
|
||||
kind: message.data['kind']?.toString() ?? '',
|
||||
fandomId: message.data['fandomId']?.toString(),
|
||||
);
|
||||
factory PushNotice.of(RemoteMessage message) {
|
||||
String? str(String key) => switch (message.data[key]?.toString()) {
|
||||
null || '' => null,
|
||||
final v => v,
|
||||
};
|
||||
return PushNotice(
|
||||
title: message.notification?.title ?? '',
|
||||
body: message.notification?.body ?? '',
|
||||
kind: str('kind') ?? '',
|
||||
fandomId: str('fandomId'),
|
||||
phase: str('phase') ?? '',
|
||||
targetPath: str('targetPath'),
|
||||
questId: str('questId'),
|
||||
campaignId: str('campaignId'),
|
||||
);
|
||||
}
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
|
||||
/// 서버가 실은 분기 키 — `briefing` · `eclipse`.
|
||||
/// 서버가 실은 분기 키 — 팬덤 토픽 `briefing` · `eclipse` · `timeattack` · `evolution`,
|
||||
/// 알림함 개인 Push 는 그 항목의 `eventType`(`eclipse` · `item_used` · `quest` · `campaign` · `evolution`).
|
||||
final String kind;
|
||||
final String? fandomId;
|
||||
|
||||
/// 탭했을 때 열 경로.
|
||||
///
|
||||
/// 브리핑 상세 화면은 아직 라우트가 없어 홈의 브리핑 블록으로 보낸다. 이클립스는
|
||||
/// 갤럭시에서 행성으로 들어가는 동선이 이미 있다.
|
||||
/// 이클립스 알림함 항목의 단계 — `start` · `defended`. 방어 성공은 위기가 아니라 결과다(N-9).
|
||||
final String phase;
|
||||
|
||||
/// 알림함 항목이 실은 이동 경로(`quests/{id}`) · 타임어택 퀘스트 · 캠페인 정산 알림의 캠페인.
|
||||
final String? targetPath;
|
||||
final String? questId;
|
||||
final String? campaignId;
|
||||
|
||||
/// 탭했을 때 먼저 갈 탭. 그 위에 [target] 화면을 연다.
|
||||
String get route => switch (kind) {
|
||||
'eclipse' => '/galaxy',
|
||||
_ => '/home',
|
||||
};
|
||||
|
||||
/// 탭했을 때 열 화면 — UX·FX v1.5 8장 「Deep Link — 공유/Push 에서 진입한 Planet/Campaign/Briefing 의
|
||||
/// 관련 화면으로」 · 9장 Eclipse 「탭 시 해당 Planet/Eclipse 로 Deep Link」 · HERO TIME 「해당 Planet 즉시
|
||||
/// 참여 CTA」. 경로 표기는 알림함과 같다(`quests/{id}` · `campaigns/{id}`), 행성은 `planets/{id}`,
|
||||
/// 진행 중 이클립스는 `eclipse/{id}`. 모르면 null — 탭만 연다(브리핑은 HOME 블록이 그 화면이다).
|
||||
String? get target {
|
||||
if (targetPath case final p? when kind == 'quest' || kind == 'campaign') {
|
||||
return p;
|
||||
}
|
||||
return switch ((kind, fandomId)) {
|
||||
('campaign', _) when campaignId != null => 'campaigns/$campaignId',
|
||||
('timeattack', _) when questId != null => 'quests/$questId',
|
||||
('eclipse', final f?) when phase != 'defended' => 'eclipse/$f',
|
||||
('eclipse' || 'evolution' || 'item_used', final f?) => 'planets/$f',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// 표시할 내용이 있는가. 데이터 전용 메시지는 화면에 띄우지 않는다.
|
||||
bool get hasText => title.isNotEmpty || body.isNotEmpty;
|
||||
|
||||
/// 원장 C-78 우선순위. 서버가 지금 싣는 `kind` 만 나눈다 — 모르는 종류는 단순 보상 유도형과 같이
|
||||
/// 가장 뒤([PushTier.other])라 상위 이벤트를 가리지 않는다.
|
||||
PushTier get tier => switch (kind) {
|
||||
'eclipse' => PushTier.crisis,
|
||||
'timeattack' => PushTier.collective,
|
||||
'evolution' => PushTier.achievement,
|
||||
_ => PushTier.other,
|
||||
};
|
||||
PushTier get tier => pushTierOf(kind, phase: phase);
|
||||
|
||||
/// 같은 사건인가 — 문구가 아니라 사건으로 본다. 진화는 팬덤 토픽 Push 와 참여자 알림함 Push 가
|
||||
/// 문구만 다르게 함께 온다(v1.5 §1.1 「동일 이벤트가 과도하게 중첩되지 않도록」).
|
||||
bool sameAs(PushNotice other) =>
|
||||
kind == other.kind &&
|
||||
title == other.title &&
|
||||
body == other.body &&
|
||||
fandomId == other.fandomId;
|
||||
fandomId == other.fandomId &&
|
||||
phase == other.phase &&
|
||||
target == other.target;
|
||||
|
||||
/// HOME 라이브 띠가 같은 사건을 띄우고 있을 때의 키([liveTickerKey]). 방어 성공은 띠에 없다.
|
||||
String? get tickerKey => fandomId == null || phase == 'defended'
|
||||
? null
|
||||
: liveTickerKey(kind, fandomId!);
|
||||
}
|
||||
|
||||
/// 라이브 띠 · Push 가 같은 사건을 가리키는 키 — Push `kind` 표기 + 팬덤.
|
||||
String liveTickerKey(String kind, String fandomId) => '$kind:$fandomId';
|
||||
|
||||
/// 원장 C-78 「Push 우선순위」 — ① 위기 ② 공동행동 ③ 성취 ④ 결과, 그 밖(단순 보상 유도형 포함)은 맨 뒤.
|
||||
enum PushTier { crisis, collective, achievement, result, other }
|
||||
|
||||
/// 원장 C-78 단계 배정 — Push 와 알림함이 같은 표를 쓴다.
|
||||
///
|
||||
/// - 위기: 이클립스 사전 경고 · 발생
|
||||
/// - 공동행동: TIME ATTACK 시작 — **임시값(해석 확인 Q-C-10 A안)**
|
||||
/// - 성취: Evolution · HERO TIME 발동(영웅의 선물, 알림함 `item_used`)
|
||||
/// - 결과: Quest 완료 · Campaign 정산 · 이클립스 **방어 성공**(통보 N-9)
|
||||
/// - 그 밖: AI 브리핑(**임시값 Q-C-10 A안** — 네 단계 예시에 없음) · 모르는 종류
|
||||
PushTier pushTierOf(String kind, {String phase = ''}) => switch (kind) {
|
||||
'eclipse' when phase == 'defended' => PushTier.result,
|
||||
'eclipse' => PushTier.crisis,
|
||||
'timeattack' => PushTier.collective,
|
||||
'evolution' || 'item_used' => PushTier.achievement,
|
||||
'quest' || 'campaign' => PushTier.result,
|
||||
_ => PushTier.other,
|
||||
};
|
||||
|
||||
/// 앱 사용 중 수신한 알림을 띄울지 — UX·FX v1.5 §1.1 「Push 와 인앱 티커는 동일 이벤트가 과도하게
|
||||
/// 중첩되지 않도록 노출 빈도를 제어하고 핵심 이벤트는 우선순위를 높게」 · 9장 Eclipse 「앱 사용 중 동일
|
||||
/// 내용의 과도한 중복 Push」 금지.
|
||||
///
|
||||
/// [showing] 은 지금 떠 있는 알림. 같은 알림은 다시 띄우지 않고, 더 낮은 순위가 높은 순위를 덮지 않는다.
|
||||
bool shouldShowInApp(PushNotice next, PushNotice? showing) =>
|
||||
showing == null ||
|
||||
(!next.sameAs(showing) && next.tier.index <= showing.tier.index);
|
||||
/// [showing] 은 지금 떠 있는 알림. 같은 사건은 다시 띄우지 않고, 더 낮은 순위가 높은 순위를 덮지 않는다.
|
||||
/// [ticker] 는 지금 보이는 HOME 라이브 띠의 사건([liveTickerKey]) — 띠가 이미 알리는 사건은 띄우지 않는다.
|
||||
bool shouldShowInApp(PushNotice next, PushNotice? showing, {String? ticker}) =>
|
||||
(ticker == null || next.tickerKey != ticker) &&
|
||||
(showing == null ||
|
||||
(!next.sameAs(showing) && next.tier.index <= showing.tier.index));
|
||||
|
||||
Reference in New Issue
Block a user