Better whats new

This commit is contained in:
LittleSheep 2024-09-03 23:07:20 +08:00
parent bd1369e72d
commit baa6b401d3
9 changed files with 243 additions and 47 deletions

View File

@ -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;
} }

View File

@ -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());
} }
} }

View File

@ -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;

View 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!);
}
}
}

View File

@ -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;

View File

@ -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')) {
final id = prefs.getInt('feed_last_read_at')!;
setState(() { setState(() {
_currentPosts = result.data _currentPostsCount = result.count;
?.map((e) => Post.fromJson(e)) _currentPosts = result.data?.map((e) => Post.fromJson(e)).toList();
.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(),
}), }),
), ),
], ],
@ -274,7 +292,6 @@ class _DashboardScreenState extends State<DashboardScreen> {
final item = _currentPosts![idx]; final item = _currentPosts![idx];
return SizedBox( return SizedBox(
width: width, width: width,
child: Card(
child: Card( child: Card(
child: PostListEntryWidget( child: PostListEntryWidget(
item: item, item: item,
@ -284,10 +301,103 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },
backgroundColor: Theme.of(context) backgroundColor:
.colorScheme Theme.of(context).colorScheme.surfaceContainerLow,
.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),

View File

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

View File

@ -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': '相簿',

View File

@ -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(