✨ Better whats new
This commit is contained in:
		@@ -2,14 +2,12 @@ import 'dart:math';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
					import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
				
			||||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
					 | 
				
			||||||
import 'package:solian/models/pagination.dart';
 | 
					import 'package:solian/models/pagination.dart';
 | 
				
			||||||
import 'package:solian/models/post.dart';
 | 
					import 'package:solian/models/post.dart';
 | 
				
			||||||
import 'package:solian/providers/content/posts.dart';
 | 
					import 'package:solian/providers/content/posts.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/last_read.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostListController extends GetxController {
 | 
					class PostListController extends GetxController {
 | 
				
			||||||
  late final SharedPreferences _prefs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  String? author;
 | 
					  String? author;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// The polling source modifier.
 | 
					  /// The polling source modifier.
 | 
				
			||||||
@@ -19,21 +17,14 @@ class PostListController extends GetxController {
 | 
				
			|||||||
  RxInt mode = 0.obs;
 | 
					  RxInt mode = 0.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// The paging controller for infinite loading.
 | 
					  /// The paging controller for infinite loading.
 | 
				
			||||||
  /// Only available when mode is `0` or `1`.
 | 
					  /// Only available when mode is `0`, `1` or `2`.
 | 
				
			||||||
  PagingController<int, Post> pagingController =
 | 
					  PagingController<int, Post> pagingController =
 | 
				
			||||||
      PagingController(firstPageKey: 0);
 | 
					      PagingController(firstPageKey: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  PostListController({this.author}) {
 | 
					  PostListController({this.author}) {
 | 
				
			||||||
    _initPreferences();
 | 
					 | 
				
			||||||
    _initPagingController();
 | 
					    _initPagingController();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _initPreferences() {
 | 
					 | 
				
			||||||
    SharedPreferences.getInstance().then((prefs) {
 | 
					 | 
				
			||||||
      _prefs = prefs;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Initialize a compatibility layer to paging controller
 | 
					  /// Initialize a compatibility layer to paging controller
 | 
				
			||||||
  void _initPagingController() {
 | 
					  void _initPagingController() {
 | 
				
			||||||
    pagingController.addPageRequestListener(_onPagingControllerRequest);
 | 
					    pagingController.addPageRequestListener(_onPagingControllerRequest);
 | 
				
			||||||
@@ -109,11 +100,7 @@ class PostListController extends GetxController {
 | 
				
			|||||||
    postList.retainWhere((x) => idx.add(x.id));
 | 
					    postList.retainWhere((x) => idx.add(x.id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var lastId = postList.map((x) => x.id).reduce(max);
 | 
					    var lastId = postList.map((x) => x.id).reduce(max);
 | 
				
			||||||
    if (_prefs.containsKey('feed_last_read_at')) {
 | 
					    Get.find<LastReadProvider>().feedLastReadAt = lastId;
 | 
				
			||||||
      final storedId = _prefs.getInt('feed_last_read_at') ?? 0;
 | 
					 | 
				
			||||||
      lastId = max(storedId, lastId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    _prefs.setInt('feed_last_read_at', lastId);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ import 'package:solian/firebase_options.dart';
 | 
				
			|||||||
import 'package:solian/platform.dart';
 | 
					import 'package:solian/platform.dart';
 | 
				
			||||||
import 'package:solian/providers/attachment_uploader.dart';
 | 
					import 'package:solian/providers/attachment_uploader.dart';
 | 
				
			||||||
import 'package:solian/providers/daily_sign.dart';
 | 
					import 'package:solian/providers/daily_sign.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/last_read.dart';
 | 
				
			||||||
import 'package:solian/providers/link_expander.dart';
 | 
					import 'package:solian/providers/link_expander.dart';
 | 
				
			||||||
import 'package:solian/providers/stickers.dart';
 | 
					import 'package:solian/providers/stickers.dart';
 | 
				
			||||||
import 'package:solian/providers/theme_switcher.dart';
 | 
					import 'package:solian/providers/theme_switcher.dart';
 | 
				
			||||||
@@ -132,5 +133,6 @@ class SolianApp extends StatelessWidget {
 | 
				
			|||||||
    Get.lazyPut(() => AttachmentUploaderController());
 | 
					    Get.lazyPut(() => AttachmentUploaderController());
 | 
				
			||||||
    Get.lazyPut(() => LinkExpandProvider());
 | 
					    Get.lazyPut(() => LinkExpandProvider());
 | 
				
			||||||
    Get.lazyPut(() => DailySignProvider());
 | 
					    Get.lazyPut(() => DailySignProvider());
 | 
				
			||||||
 | 
					    Get.lazyPut(() => LastReadProvider());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,22 @@ class PostProvider extends GetConnect {
 | 
				
			|||||||
    httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
 | 
					    httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Response> seeWhatsNew(int pivot) async {
 | 
				
			||||||
 | 
					    GetConnect client;
 | 
				
			||||||
 | 
					    final AuthProvider auth = Get.find();
 | 
				
			||||||
 | 
					    if (auth.isAuthorized.value) {
 | 
				
			||||||
 | 
					      client = auth.configureClient('co');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      client = ServiceFinder.configureClient('co');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final resp = await client.get('/whats-new?pivot=$pivot');
 | 
				
			||||||
 | 
					    if (resp.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw RequestException(resp);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return resp;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<Response> listRecommendations(int page,
 | 
					  Future<Response> listRecommendations(int page,
 | 
				
			||||||
      {String? realm, String? channel}) async {
 | 
					      {String? realm, String? channel}) async {
 | 
				
			||||||
    GetConnect client;
 | 
					    GetConnect client;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								lib/providers/last_read.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								lib/providers/last_read.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:shared_preferences/shared_preferences.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LastReadProvider extends GetxController {
 | 
				
			||||||
 | 
					  int? _feedLastReadAt;
 | 
				
			||||||
 | 
					  int? _messagesLastReadAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int? get feedLastReadAt => _feedLastReadAt;
 | 
				
			||||||
 | 
					  int? get messagesLastReadAt => _messagesLastReadAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set feedLastReadAt(int? value) {
 | 
				
			||||||
 | 
					    if (value == _feedLastReadAt) return;
 | 
				
			||||||
 | 
					    _feedLastReadAt = max(_feedLastReadAt ?? 0, value ?? 0);
 | 
				
			||||||
 | 
					    if (value != _feedLastReadAt) _saveToStorage();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set messagesLastReadAt(int? value) {
 | 
				
			||||||
 | 
					    if (value == _messagesLastReadAt) return;
 | 
				
			||||||
 | 
					    _messagesLastReadAt = max(_messagesLastReadAt ?? 0, value ?? 0);
 | 
				
			||||||
 | 
					    if (value != _messagesLastReadAt) _saveToStorage();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LastReadProvider() {
 | 
				
			||||||
 | 
					    _revertFromStorage();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _revertFromStorage() async {
 | 
				
			||||||
 | 
					    final prefs = await SharedPreferences.getInstance();
 | 
				
			||||||
 | 
					    if (prefs.containsKey('feed_last_read_at')) {
 | 
				
			||||||
 | 
					      _feedLastReadAt = prefs.getInt('feed_last_read_at')!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (prefs.containsKey('messages_last_read_at')) {
 | 
				
			||||||
 | 
					      _messagesLastReadAt = prefs.getInt('messages_last_read_at');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _saveToStorage() async {
 | 
				
			||||||
 | 
					    final prefs = await SharedPreferences.getInstance();
 | 
				
			||||||
 | 
					    if (_feedLastReadAt != null) {
 | 
				
			||||||
 | 
					      prefs.setInt('feed_last_read_at', _feedLastReadAt!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_messagesLastReadAt != null) {
 | 
				
			||||||
 | 
					      prefs.setInt('messages_last_read_at', _messagesLastReadAt!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -17,6 +17,27 @@ Future<MessageHistoryDb> createHistoryDb() async {
 | 
				
			|||||||
      .addMigrations([migration1to2]).build();
 | 
					      .addMigrations([migration1to2]).build();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
 | 
				
			||||||
 | 
					  final AuthProvider auth = Get.find();
 | 
				
			||||||
 | 
					  if (auth.isAuthorized.isFalse) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final client = auth.configureClient('messaging');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final resp = await client.get(
 | 
				
			||||||
 | 
					    '/whats-new?pivot=$pivot&take=$take',
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (resp.statusCode != 200) {
 | 
				
			||||||
 | 
					    throw RequestException(resp);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final PaginationResult response = PaginationResult.fromJson(resp.body);
 | 
				
			||||||
 | 
					  final result =
 | 
				
			||||||
 | 
					      response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (result, response.count);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
 | 
					Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
 | 
				
			||||||
  final AuthProvider auth = Get.find();
 | 
					  final AuthProvider auth = Get.find();
 | 
				
			||||||
  if (auth.isAuthorized.isFalse) return null;
 | 
					  if (auth.isAuthorized.isFalse) return null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,24 @@
 | 
				
			|||||||
import 'dart:math';
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					import 'dart:math' hide log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:google_fonts/google_fonts.dart';
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
import 'package:intl/intl.dart';
 | 
					import 'package:intl/intl.dart';
 | 
				
			||||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
					 | 
				
			||||||
import 'package:solian/exts.dart';
 | 
					import 'package:solian/exts.dart';
 | 
				
			||||||
import 'package:solian/models/daily_sign.dart';
 | 
					import 'package:solian/models/daily_sign.dart';
 | 
				
			||||||
 | 
					import 'package:solian/models/event.dart';
 | 
				
			||||||
import 'package:solian/models/pagination.dart';
 | 
					import 'package:solian/models/pagination.dart';
 | 
				
			||||||
import 'package:solian/models/post.dart';
 | 
					import 'package:solian/models/post.dart';
 | 
				
			||||||
import 'package:solian/providers/content/posts.dart';
 | 
					import 'package:solian/providers/content/posts.dart';
 | 
				
			||||||
import 'package:solian/providers/daily_sign.dart';
 | 
					import 'package:solian/providers/daily_sign.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/last_read.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/message/adaptor.dart';
 | 
				
			||||||
import 'package:solian/providers/websocket.dart';
 | 
					import 'package:solian/providers/websocket.dart';
 | 
				
			||||||
import 'package:solian/router.dart';
 | 
					import 'package:solian/router.dart';
 | 
				
			||||||
import 'package:solian/screens/account/notification.dart';
 | 
					import 'package:solian/screens/account/notification.dart';
 | 
				
			||||||
 | 
					import 'package:solian/widgets/chat/chat_event.dart';
 | 
				
			||||||
import 'package:solian/widgets/posts/post_list.dart';
 | 
					import 'package:solian/widgets/posts/post_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DashboardScreen extends StatefulWidget {
 | 
					class DashboardScreen extends StatefulWidget {
 | 
				
			||||||
@@ -24,6 +29,7 @@ class DashboardScreen extends StatefulWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _DashboardScreenState extends State<DashboardScreen> {
 | 
					class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			||||||
 | 
					  late final LastReadProvider _lastRead = Get.find();
 | 
				
			||||||
  late final WebSocketProvider _ws = Get.find();
 | 
					  late final WebSocketProvider _ws = Get.find();
 | 
				
			||||||
  late final PostProvider _posts = Get.find();
 | 
					  late final PostProvider _posts = Get.find();
 | 
				
			||||||
  late final DailySignProvider _dailySign = Get.find();
 | 
					  late final DailySignProvider _dailySign = Get.find();
 | 
				
			||||||
@@ -32,20 +38,31 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
      Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
					      Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<Post>? _currentPosts;
 | 
					  List<Post>? _currentPosts;
 | 
				
			||||||
 | 
					  int? _currentPostsCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _pullPosts() async {
 | 
					  Future<void> _pullPosts() async {
 | 
				
			||||||
    final prefs = await SharedPreferences.getInstance();
 | 
					    if (_lastRead.feedLastReadAt == null) return;
 | 
				
			||||||
    final resp = await _posts.listRecommendations(0);
 | 
					    log('[Dashboard] Pulling posts with pivot: ${_lastRead.feedLastReadAt}');
 | 
				
			||||||
 | 
					    final resp = await _posts.seeWhatsNew(_lastRead.feedLastReadAt!);
 | 
				
			||||||
    final result = PaginationResult.fromJson(resp.body);
 | 
					    final result = PaginationResult.fromJson(resp.body);
 | 
				
			||||||
    if (prefs.containsKey('feed_last_read_at')) {
 | 
					    setState(() {
 | 
				
			||||||
      final id = prefs.getInt('feed_last_read_at')!;
 | 
					      _currentPostsCount = result.count;
 | 
				
			||||||
      setState(() {
 | 
					      _currentPosts = result.data?.map((e) => Post.fromJson(e)).toList();
 | 
				
			||||||
        _currentPosts = result.data
 | 
					    });
 | 
				
			||||||
            ?.map((e) => Post.fromJson(e))
 | 
					  }
 | 
				
			||||||
            .where((x) => x.id > id)
 | 
					
 | 
				
			||||||
            .toList();
 | 
					  List<Event>? _currentMessages;
 | 
				
			||||||
      });
 | 
					  int? _currentMessagesCount;
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _pullMessages() async {
 | 
				
			||||||
 | 
					    if (_lastRead.messagesLastReadAt == null) return;
 | 
				
			||||||
 | 
					    log('[Dashboard] Pulling messages with pivot: ${_lastRead.messagesLastReadAt}');
 | 
				
			||||||
 | 
					    final out = await getWhatsNewEvents(_lastRead.messagesLastReadAt!);
 | 
				
			||||||
 | 
					    if (out == null) return;
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _currentMessages = out.$1;
 | 
				
			||||||
 | 
					      _currentMessagesCount = out.$2;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _signingDaily = true;
 | 
					  bool _signingDaily = true;
 | 
				
			||||||
@@ -77,6 +94,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    _pullPosts();
 | 
					    _pullPosts();
 | 
				
			||||||
 | 
					    _pullMessages();
 | 
				
			||||||
    _pullDaily();
 | 
					    _pullDaily();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,7 +144,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
                    ).paddingSymmetric(horizontal: 9),
 | 
					                    ).paddingSymmetric(horizontal: 9),
 | 
				
			||||||
            ).paddingOnly(left: 4),
 | 
					            ).paddingOnly(left: 4),
 | 
				
			||||||
            title: _signRecord == null
 | 
					            title: _signRecord == null
 | 
				
			||||||
                ? const Text('诸事不宜')
 | 
					                ? const Text('签到')
 | 
				
			||||||
                : Text(_signRecord!.overviewSuggestion),
 | 
					                : Text(_signRecord!.overviewSuggestion),
 | 
				
			||||||
            subtitle: _signRecord == null
 | 
					            subtitle: _signRecord == null
 | 
				
			||||||
                ? const Text('今日未拜访佛祖')
 | 
					                ? const Text('今日未拜访佛祖')
 | 
				
			||||||
@@ -250,8 +268,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
                            .copyWith(fontSize: 18),
 | 
					                            .copyWith(fontSize: 18),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      Text(
 | 
					                      Text(
 | 
				
			||||||
                        'notificationUnreadCount'.trParams({
 | 
					                        'feedUnreadCount'.trParams({
 | 
				
			||||||
                          'count': (_currentPosts?.length ?? 0).toString(),
 | 
					                          'count': (_currentPostsCount ?? 0).toString(),
 | 
				
			||||||
                        }),
 | 
					                        }),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
@@ -275,19 +293,111 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
                    return SizedBox(
 | 
					                    return SizedBox(
 | 
				
			||||||
                      width: width,
 | 
					                      width: width,
 | 
				
			||||||
                      child: Card(
 | 
					                      child: Card(
 | 
				
			||||||
                        child: Card(
 | 
					                        child: PostListEntryWidget(
 | 
				
			||||||
                          child: PostListEntryWidget(
 | 
					                          item: item,
 | 
				
			||||||
                            item: item,
 | 
					                          isClickable: true,
 | 
				
			||||||
                            isClickable: true,
 | 
					                          isShowEmbed: true,
 | 
				
			||||||
                            isShowEmbed: true,
 | 
					                          isNestedClickable: true,
 | 
				
			||||||
                            isNestedClickable: true,
 | 
					                          onUpdate: (_) {
 | 
				
			||||||
                            onUpdate: (_) {
 | 
					                            _pullPosts();
 | 
				
			||||||
                              _pullPosts();
 | 
					                          },
 | 
				
			||||||
                            },
 | 
					                          backgroundColor:
 | 
				
			||||||
                            backgroundColor: Theme.of(context)
 | 
					                              Theme.of(context).colorScheme.surfaceContainerLow,
 | 
				
			||||||
                                .colorScheme
 | 
					                        ),
 | 
				
			||||||
                                .surfaceContainerLow,
 | 
					                      ).paddingSymmetric(horizontal: 8),
 | 
				
			||||||
                          ),
 | 
					                    );
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        if (_currentMessages?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					          Column(
 | 
				
			||||||
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Row(
 | 
				
			||||||
 | 
					                mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        'messages'.tr,
 | 
				
			||||||
 | 
					                        style: Theme.of(context)
 | 
				
			||||||
 | 
					                            .textTheme
 | 
				
			||||||
 | 
					                            .titleMedium!
 | 
				
			||||||
 | 
					                            .copyWith(fontSize: 18),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        'messagesUnreadCount'.trParams({
 | 
				
			||||||
 | 
					                          'count': (_currentMessagesCount ?? 0).toString(),
 | 
				
			||||||
 | 
					                        }),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  IconButton(
 | 
				
			||||||
 | 
					                    icon: const Icon(Icons.arrow_forward),
 | 
				
			||||||
 | 
					                    onPressed: () {
 | 
				
			||||||
 | 
					                      AppRouter.instance.goNamed('chat');
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ).paddingOnly(left: 18, right: 18, bottom: 8),
 | 
				
			||||||
 | 
					              SizedBox(
 | 
				
			||||||
 | 
					                height: 240,
 | 
				
			||||||
 | 
					                width: width,
 | 
				
			||||||
 | 
					                child: ListView.builder(
 | 
				
			||||||
 | 
					                  scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                  itemCount: _currentMessages!.length,
 | 
				
			||||||
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					                    final item = _currentMessages![idx];
 | 
				
			||||||
 | 
					                    return SizedBox(
 | 
				
			||||||
 | 
					                      width: width,
 | 
				
			||||||
 | 
					                      child: Card(
 | 
				
			||||||
 | 
					                        child: Column(
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            ListTile(
 | 
				
			||||||
 | 
					                              tileColor: Theme.of(context)
 | 
				
			||||||
 | 
					                                  .colorScheme
 | 
				
			||||||
 | 
					                                  .surfaceContainerHigh,
 | 
				
			||||||
 | 
					                              shape: const RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                                  borderRadius: BorderRadius.only(
 | 
				
			||||||
 | 
					                                topLeft: Radius.circular(8),
 | 
				
			||||||
 | 
					                                topRight: Radius.circular(8),
 | 
				
			||||||
 | 
					                              )),
 | 
				
			||||||
 | 
					                              leading: CircleAvatar(
 | 
				
			||||||
 | 
					                                backgroundColor: item.channel!.realmId == null
 | 
				
			||||||
 | 
					                                    ? Theme.of(context).colorScheme.primary
 | 
				
			||||||
 | 
					                                    : Colors.transparent,
 | 
				
			||||||
 | 
					                                radius: 20,
 | 
				
			||||||
 | 
					                                child: FaIcon(
 | 
				
			||||||
 | 
					                                  FontAwesomeIcons.hashtag,
 | 
				
			||||||
 | 
					                                  color: item.channel!.realmId == null
 | 
				
			||||||
 | 
					                                      ? Theme.of(context).colorScheme.onPrimary
 | 
				
			||||||
 | 
					                                      : Theme.of(context).colorScheme.primary,
 | 
				
			||||||
 | 
					                                  size: 16,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              contentPadding:
 | 
				
			||||||
 | 
					                                  const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
 | 
					                              title: Text(item.channel!.name),
 | 
				
			||||||
 | 
					                              subtitle: Text(item.channel!.description),
 | 
				
			||||||
 | 
					                              onTap: () {
 | 
				
			||||||
 | 
					                                AppRouter.instance.pushNamed(
 | 
				
			||||||
 | 
					                                  'channelChat',
 | 
				
			||||||
 | 
					                                  pathParameters: {
 | 
				
			||||||
 | 
					                                    'alias': item.channel!.alias
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                  queryParameters: {
 | 
				
			||||||
 | 
					                                    if (item.channel!.realmId != null)
 | 
				
			||||||
 | 
					                                      'realm': item.channel!.realm!.alias,
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            ChatEvent(item: item).paddingOnly(
 | 
				
			||||||
 | 
					                                bottom: 8, top: 16, left: 8, right: 8),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ).paddingSymmetric(horizontal: 8),
 | 
					                      ).paddingSymmetric(horizontal: 8),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
@@ -305,8 +415,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
              '占卜多少都是玩,人生还得靠自己',
 | 
					              '占卜多少都是玩,人生还得靠自己',
 | 
				
			||||||
              style:
 | 
					              style: GoogleFonts.notoSerifHk(
 | 
				
			||||||
                  GoogleFonts.notoSerifHk(color: _unFocusColor, fontSize: 12),
 | 
					                color: _unFocusColor,
 | 
				
			||||||
 | 
					                fontSize: 12,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ).paddingAll(8),
 | 
					        ).paddingAll(8),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,9 @@ const i18nEnglish = {
 | 
				
			|||||||
  'feedSearch': 'Search Feed',
 | 
					  'feedSearch': 'Search Feed',
 | 
				
			||||||
  'feedSearchWithTag': 'Searching with tag #@key',
 | 
					  'feedSearchWithTag': 'Searching with tag #@key',
 | 
				
			||||||
  'feedSearchWithCategory': 'Searching in category @category',
 | 
					  'feedSearchWithCategory': 'Searching in category @category',
 | 
				
			||||||
 | 
					  'feedUnreadCount': '@count posts you may missed',
 | 
				
			||||||
 | 
					  'messages': 'Messages',
 | 
				
			||||||
 | 
					  'messagesUnreadCount': '@count messages unread',
 | 
				
			||||||
  'visitProfilePage': 'Visit Profile Page',
 | 
					  'visitProfilePage': 'Visit Profile Page',
 | 
				
			||||||
  'profilePosts': 'Posts',
 | 
					  'profilePosts': 'Posts',
 | 
				
			||||||
  'profileAlbum': 'Album',
 | 
					  'profileAlbum': 'Album',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,9 @@ const i18nSimplifiedChinese = {
 | 
				
			|||||||
  'feedSearch': '搜索资讯',
 | 
					  'feedSearch': '搜索资讯',
 | 
				
			||||||
  'feedSearchWithTag': '检索带有 #@key 标签的资讯',
 | 
					  'feedSearchWithTag': '检索带有 #@key 标签的资讯',
 | 
				
			||||||
  'feedSearchWithCategory': '检索位于分类 @category 的资讯',
 | 
					  'feedSearchWithCategory': '检索位于分类 @category 的资讯',
 | 
				
			||||||
 | 
					  'feedUnreadCount': '@count 条你可能错过的帖子',
 | 
				
			||||||
 | 
					  'messages': '消息',
 | 
				
			||||||
 | 
					  'messagesUnreadCount': '@count 条未读的消息',
 | 
				
			||||||
  'visitProfilePage': '造访个人主页',
 | 
					  'visitProfilePage': '造访个人主页',
 | 
				
			||||||
  'profilePosts': '帖子',
 | 
					  'profilePosts': '帖子',
 | 
				
			||||||
  'profileAlbum': '相簿',
 | 
					  'profileAlbum': '相簿',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
 | 
				
			|||||||
import 'package:solian/controllers/chat_events_controller.dart';
 | 
					import 'package:solian/controllers/chat_events_controller.dart';
 | 
				
			||||||
import 'package:solian/models/channel.dart';
 | 
					import 'package:solian/models/channel.dart';
 | 
				
			||||||
import 'package:solian/models/event.dart';
 | 
					import 'package:solian/models/event.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/last_read.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/chat_event.dart';
 | 
					import 'package:solian/widgets/chat/chat_event.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/chat_event_action.dart';
 | 
					import 'package:solian/widgets/chat/chat_event_action.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,6 +40,9 @@ class ChatEventList extends StatelessWidget {
 | 
				
			|||||||
            key: Key('chat-history#${channel.id}'),
 | 
					            key: Key('chat-history#${channel.id}'),
 | 
				
			||||||
            itemCount: chatController.currentEvents.length,
 | 
					            itemCount: chatController.currentEvents.length,
 | 
				
			||||||
            itemBuilder: (context, index) {
 | 
					            itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					              Get.find<LastReadProvider>().messagesLastReadAt =
 | 
				
			||||||
 | 
					                  chatController.currentEvents[index].id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              bool isMerged = false, hasMerged = false;
 | 
					              bool isMerged = false, hasMerged = false;
 | 
				
			||||||
              if (index > 0) {
 | 
					              if (index > 0) {
 | 
				
			||||||
                hasMerged = _checkMessageMergeable(
 | 
					                hasMerged = _checkMessageMergeable(
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user