Compare commits

...

20 Commits

Author SHA1 Message Date
8e76ff3f84 Optimize user loading api usage 2025-02-27 20:51:47 +08:00
bd26602299 Code highlighting 2025-02-26 23:29:02 +08:00
52ab1d0d10 🐛 Fix chat last message displaying inconsistences 2025-02-26 00:29:35 +08:00
f746e06f65 ⚗️ Experimental user first badge showing on chat 2025-02-26 00:25:42 +08:00
d11069a2be 🐛 Bug fixes on notification page 2025-02-26 00:00:53 +08:00
d6dc487d9e Latex Rendering, closed #9 2025-02-25 23:49:48 +08:00
a07c7cdede 🐛 Fix infinite loading own sticker 2025-02-25 22:56:30 +08:00
acbc125dec 🚀 Launch 2.3.2+75 2025-02-24 23:21:06 +08:00
ad0ee971c1 Desktop mute notification
🐛 Bug fixes on tray icon
2025-02-24 22:46:02 +08:00
52d6bb083e 🐛 Fix macos titlebar not centered 2025-02-24 22:38:08 +08:00
2027eab49b 💄 Optimize displaying of message 2025-02-24 22:35:14 +08:00
566ebde1dd 🐛 Fix windows tray issue 2025-02-24 21:59:41 +08:00
9e039cc532 🐛 Fix editing message 2025-02-24 21:31:12 +08:00
c4b95d7084 🐛 Fix account settings screen error cause by locale 2025-02-24 21:25:12 +08:00
a66129a9ba 🐛 Bug fixes 2025-02-24 21:18:49 +08:00
44e1a8bf67 🚀 Launch 2.3.2+74 2025-02-23 22:45:01 +08:00
efcfd3f57d 🚀 Launch 2.3.2+73 2025-02-23 21:37:33 +08:00
84759715a4 💄 Not showing notification when in the channel 2025-02-23 21:19:34 +08:00
fda09382dd 💄 Hide unread count auto after entering channel 2025-02-23 21:10:32 +08:00
2c5dd0563a 🐛 Fix checking for update db issue 2025-02-23 21:10:18 +08:00
25 changed files with 558 additions and 217 deletions

View File

@ -719,6 +719,7 @@
"stickersNewDescription": "Create a new sticker belongs to this pack.", "stickersNewDescription": "Create a new sticker belongs to this pack.",
"stickersPackNew": "New Sticker Pack", "stickersPackNew": "New Sticker Pack",
"trayMenuShow": "Show", "trayMenuShow": "Show",
"trayMenuMuteNotification": "Do Not Disturb",
"update": "Update", "update": "Update",
"forceUpdate": "Force Update", "forceUpdate": "Force Update",
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available." "forceUpdateDescription": "Force to show the application update popup, even the new version is not available."

View File

@ -717,6 +717,7 @@
"stickersNewDescription": "创建一个新的贴图。", "stickersNewDescription": "创建一个新的贴图。",
"stickersPackNew": "新建贴图包", "stickersPackNew": "新建贴图包",
"trayMenuShow": "显示", "trayMenuShow": "显示",
"trayMenuMuteNotification": "静音通知",
"update": "更新", "update": "更新",
"forceUpdate": "强制更新", "forceUpdate": "强制更新",
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。"

View File

@ -717,6 +717,7 @@
"stickersNewDescription": "創建一個新的貼圖。", "stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包", "stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示", "trayMenuShow": "顯示",
"trayMenuMuteNotification": "靜音通知",
"update": "更新", "update": "更新",
"forceUpdate": "強制更新", "forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"

View File

@ -717,6 +717,7 @@
"stickersNewDescription": "創建一個新的貼圖。", "stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包", "stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示", "trayMenuShow": "顯示",
"trayMenuMuteNotification": "靜音通知",
"update": "更新", "update": "更新",
"forceUpdate": "強制更新", "forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。" "forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"

View File

@ -235,7 +235,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.49.0) - sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
@ -445,7 +445,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@ -194,9 +194,11 @@ class ChatMessageController extends ChangeNotifier {
channelId: channel!.id, channelId: channel!.id,
createdAt: Value(message.createdAt), createdAt: Value(message.createdAt),
), ),
onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom( onConflict: DoUpdate(
content: Constant(jsonEncode(message.toJson())), (_) => SnLocalChatMessageCompanion.custom(
)), content: Constant(jsonEncode(message.toJson())),
),
),
); );
} else { } else {
incomeStrandedQueue.add(message); incomeStrandedQueue.add(message);
@ -212,21 +214,21 @@ class ChatMessageController extends ChangeNotifier {
final idx = final idx =
messages.indexWhere((x) => x.id == message.relatedEventId); messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) { if (idx != -1) {
final newBody = message.body; final newBody = Map<String, dynamic>.from(message.body);
newBody.remove('related_event'); newBody.remove('related_event');
messages[idx] = messages[idx].copyWith( messages[idx] = messages[idx].copyWith(
body: newBody, body: newBody,
updatedAt: message.updatedAt, updatedAt: message.updatedAt,
); );
if (message.relatedEventId != null) { }
await (_dt.db.snLocalChatMessage.update() if (message.relatedEventId != null) {
..where((e) => e.id.equals(message.relatedEventId!))) await (_dt.db.snLocalChatMessage.update()
.write( ..where((e) => e.id.equals(message.relatedEventId!)))
SnLocalChatMessageCompanion.custom( .write(
content: Constant(jsonEncode(messages[idx].toJson())), SnLocalChatMessageCompanion.custom(
), content: Constant(jsonEncode(messages[idx].toJson())),
); ),
} );
} }
} }
case 'messages.delete': case 'messages.delete':
@ -322,6 +324,7 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners(); notifyListeners();
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select() final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
..where((e) => e.channelId.equals(channel!.id))
..limit(1) ..limit(1)
..orderBy([ ..orderBy([
(e) => (e) =>

View File

@ -333,6 +333,31 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
} }
} }
final Menu _appTrayMenu = Menu(
items: [
MenuItem(
key: 'version_label',
label: 'Solian',
disabled: true,
),
MenuItem.separator(),
MenuItem.checkbox(
checked: false,
key: 'mute_notification',
label: 'trayMenuMuteNotification'.tr(),
),
MenuItem.separator(),
MenuItem(
key: 'window_show',
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
],
);
Future<void> _trayInitialization() async { Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
@ -344,32 +369,20 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
trayManager.addListener(this); trayManager.addListener(this);
await trayManager.setIcon(icon); await trayManager.setIcon(icon);
Menu menu = Menu( _appTrayMenu.items![0] = MenuItem(
items: [ key: 'version_label',
MenuItem( label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
key: 'version_label', disabled: true,
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
disabled: true,
),
MenuItem.separator(),
MenuItem(
key: 'window_show',
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
],
); );
await trayManager.setContextMenu(menu);
await trayManager.setContextMenu(_appTrayMenu);
} }
Future<void> _notifyInitialization() async { Future<void> _notifyInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup( await localNotifier.setup(
appName: 'solian', appName: 'Solian',
shortcutPolicy: ShortcutPolicy.requireCreate, shortcutPolicy: ShortcutPolicy.requireCreate,
); );
} }
@ -424,12 +437,23 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
@override @override
void onTrayMenuItemClick(MenuItem menuItem) { void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) { switch (menuItem.key) {
case 'mute_notification':
final nty = context.read<NotificationProvider>();
nty.isMuted = !nty.isMuted;
_appTrayMenu.items![2].checked = nty.isMuted;
trayManager.setContextMenu(_appTrayMenu);
break;
case 'window_show': case 'window_show':
appWindow.show(); // To prevent the window from being hide after just show on macOS
Timer(const Duration(milliseconds: 100), () => appWindow.show());
break; break;
case 'exit': case 'exit':
_appLifecycleListener?.dispose(); _appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop'); if (Platform.isWindows) {
appWindow.close();
} else {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
break; break;
} }
} }

View File

@ -78,10 +78,25 @@ class NotificationProvider extends ChangeNotifier {
int showingTrayCount = 0; int showingTrayCount = 0;
List<SnNotification> notifications = List.empty(growable: true); List<SnNotification> notifications = List.empty(growable: true);
int? skippableNotifyChannel;
bool isMuted = false;
void listen() { void listen() {
_ws.pk.stream.listen((event) { _ws.pk.stream.listen((event) {
if (event.method == 'notifications.new') { if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!); final notification = SnNotification.fromJson(event.payload!);
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
if (notification.topic == 'messaging.message' &&
skippableNotifyChannel != null) {
if (notification.metadata['channel_id'] != null &&
notification.metadata['channel_id'] == skippableNotifyChannel) {
return;
}
}
if (showingCount < 0) showingCount = 0; if (showingCount < 0) showingCount = 0;
showingCount++; showingCount++;
showingTrayCount++; showingTrayCount++;
@ -92,10 +107,8 @@ class NotificationProvider extends ChangeNotifier {
}); });
notifyListeners(); notifyListeners();
updateTray(); updateTray();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
if (!kIsWeb) { if (!kIsWeb && !isMuted) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
LocalNotification notify = LocalNotification( LocalNotification notify = LocalNotification(
title: notification.title, title: notification.title,

View File

@ -41,7 +41,8 @@ class SnAttachmentProvider {
return out; return out;
} }
Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async { Future<List<SnAttachment?>> getMultiple(List<String> rids,
{noCache = false}) async {
final result = List<SnAttachment?>.filled(rids.length, null); final result = List<SnAttachment?>.filled(rids.length, null);
final Map<String, int> randomMapping = {}; final Map<String, int> randomMapping = {};
for (int i = 0; i < rids.length; i++) { for (int i = 0; i < rids.length; i++) {
@ -62,8 +63,10 @@ class SnAttachmentProvider {
'id': pendingFetch.join(','), 'id': pendingFetch.join(','),
}, },
); );
final List<SnAttachment?> out = final List<SnAttachment?> out = resp.data['data']
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList(); .map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
.cast<SnAttachment?>()
.toList();
for (final item in out) { for (final item in out) {
if (item == null) continue; if (item == null) continue;
@ -77,7 +80,13 @@ class SnAttachmentProvider {
return result; return result;
} }
static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'}; static Map<String, String> mimetypeOverrides = {
'mov': 'video/quicktime',
'mp4': 'video/mp4',
'm4a': 'audio/mp4',
'apng': 'image/apng',
'webp': 'image/webp',
};
Future<SnAttachment> directUploadOne( Future<SnAttachment> directUploadOne(
Uint8List data, Uint8List data,
@ -89,8 +98,11 @@ class SnAttachmentProvider {
bool analyzeNow = false, bool analyzeNow = false,
}) async { }) async {
final filePayload = MultipartFile.fromBytes(data, filename: filename); final filePayload = MultipartFile.fromBytes(data, filename: filename);
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; final fileAlt = filename.contains('.')
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); ? filename.substring(0, filename.lastIndexOf('.'))
: filename;
final fileExt =
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
String? mimetypeOverride; String? mimetypeOverride;
if (mimetype != null) { if (mimetype != null) {
@ -127,8 +139,11 @@ class SnAttachmentProvider {
Map<String, dynamic>? metadata, { Map<String, dynamic>? metadata, {
String? mimetype, String? mimetype,
}) async { }) async {
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; final fileAlt = filename.contains('.')
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); ? filename.substring(0, filename.lastIndexOf('.'))
: filename;
final fileExt =
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
String? mimetypeOverride; String? mimetypeOverride;
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) { if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
@ -146,7 +161,10 @@ class SnAttachmentProvider {
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
}); });
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int); return (
SnAttachmentFragment.fromJson(resp.data['meta']),
resp.data['chunk_size'] as int
);
} }
Future<dynamic> _chunkedUploadOnePart( Future<dynamic> _chunkedUploadOnePart(
@ -197,7 +215,10 @@ class SnAttachmentProvider {
(entry.value + 1) * chunkSize, (entry.value + 1) * chunkSize,
await file.length(), await file.length(),
); );
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList()); final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)
.toList());
final result = await _chunkedUploadOnePart( final result = await _chunkedUploadOnePart(
data, data,

View File

@ -27,8 +27,11 @@ class UserDirectoryProvider {
plannedQuery.add(item); plannedQuery.add(item);
} }
} }
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')}); if (plannedQuery.isEmpty) return out;
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList(); final resp = await _sn.client
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded =
resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
var sideIdx = 0; var sideIdx = 0;
for (var idx = 0; idx < out.length; idx++) { for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue; if (out[idx] != null) continue;

View File

@ -54,14 +54,20 @@ class AccountSettingsScreen extends StatelessWidget {
child: DropdownButton2<Locale?>( child: DropdownButton2<Locale?>(
isExpanded: true, isExpanded: true,
items: [ items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) { ...EasyLocalization.of(context)!
.supportedLocales
.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>( return DropdownMenuItem<Locale?>(
value: Locale.parse(ele.toString()), value: Locale.parse(ele.toString()),
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), child: Text('${ele.languageCode}-${ele.countryCode}')
.fontSize(14),
); );
}), }),
], ],
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'), value: ua.user?.language != null
? (Locale.tryParse(ua.user!.language) ??
Locale.parse('en-US'))
: Locale.parse('en-US'),
onChanged: (Locale? value) { onChanged: (Locale? value) {
if (value == null) return; if (value == null) return;
_setAccountLanguage(context, value); _setAccountLanguage(context, value);

View File

@ -43,7 +43,8 @@ class UserScreen extends StatefulWidget {
State<UserScreen> createState() => _UserScreenState(); State<UserScreen> createState() => _UserScreenState();
} }
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { class _UserScreenState extends State<UserScreen>
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
SnAccount? _account; SnAccount? _account;
@ -67,7 +68,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<List<SnCheckInRecord>> _getCheckInRecords() async { Future<List<SnCheckInRecord>> _getCheckInRecords() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); final resp =
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
return List.from( return List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
); );
@ -98,7 +100,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}'); final resp =
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
_publishers = List<SnPublisher>.from( _publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
@ -144,7 +147,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
'related': _account!.name, 'related': _account!.name,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -160,9 +164,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {}); await rel.updateRelationship(
_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -188,12 +194,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); _appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@ -260,18 +268,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _account!.nick, text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.titleLarge!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: '@${_account!.name}', text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.bodySmall!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
]), ]),
), ),
@ -339,7 +349,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: ButtonStyle( style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
), ),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
@ -399,7 +410,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey, color: (_status?.isOnline ?? false)
? Colors.green
: Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text( Text(
@ -409,7 +422,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
: 'accountStatusOffline'.tr() : 'accountStatusOffline'.tr()
: 'loading'.tr(), : 'loading'.tr(),
), ),
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null) if (_status != null &&
!_status!.isOnline &&
_status!.lastSeenAt != null)
Text( Text(
'accountStatusLastSeen'.tr(args: [ 'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null _status!.lastSeenAt != null
@ -429,11 +444,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
(ele) => Tooltip( (ele) => Tooltip(
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ??
'unknown'.tr()),
if (ele.metadata['title'] != null) if (ele.metadata['title'] != null)
TextSpan( TextSpan(
text: '\n${ele.metadata['title']}', text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold),
), ),
TextSpan(text: '\n'), TextSpan(text: '\n'),
TextSpan( TextSpan(
@ -442,7 +460,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
], ],
), ),
child: Icon( child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[ele.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3, color: kBadgesMeta[ele.type]?.$3,
fill: 1, fill: 1,
), ),
@ -458,7 +477,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]), Text('publisherJoinedAt').tr(args: [
DateFormat('y/M/d').format(_account!.createdAt)
]),
], ],
), ),
Row( Row(
@ -491,17 +512,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.star), const Icon(Symbols.star),
const Gap(8), const Gap(8),
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), Text(
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
const Gap(8), const Gap(8),
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5), Text(calcLevelUpProgressLevel(
_account?.profile?.experience ?? 0))
.fontSize(11)
.opacity(0.5),
const Gap(8), const Gap(8),
Container( Container(
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160), constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: calcLevelUpProgress(_account?.profile?.experience ?? 0), value: calcLevelUpProgress(
_account?.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainer,
).alignment(Alignment.centerLeft), ).alignment(Alignment.centerLeft),
), ),
], ],
@ -515,6 +543,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
const SliverGap(12), const SliverGap(12),
SliverToBoxAdapter( SliverToBoxAdapter(
child: FutureBuilder<List<SnCheckInRecord>>( child: FutureBuilder<List<SnCheckInRecord>>(
key: GlobalKey(),
future: _getCheckInRecords(), future: _getCheckInRecords(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink(); if (!snapshot.hasData) return const SizedBox.shrink();
@ -522,7 +551,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
return Text( return Text(
'accountCheckInNoRecords', 'accountCheckInNoRecords',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8); )
.tr()
.fontWeight(FontWeight.bold)
.center()
.padding(horizontal: 20, vertical: 8);
} }
final records = snapshot.data!; final records = snapshot.data!;
return SizedBox( return SizedBox(
@ -544,7 +577,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('accountBadge')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox( SizedBox(
height: 80, height: 80,
width: double.infinity, width: double.infinity,
@ -558,7 +595,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: Icon( leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[badge.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[badge.type]?.$3, color: kBadgesMeta[badge.type]?.$3,
fill: 1, fill: 1,
), ),
@ -568,7 +606,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
subtitle: badge.metadata['title'] != null subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title']) ? Text(badge.metadata['title'])
: Text( : Text(
DateFormat('y/M/d').format(badge.createdAt), DateFormat('y/M/d')
.format(badge.createdAt),
), ),
), ),
), ),
@ -664,7 +703,8 @@ class CheckInRecordChart extends StatelessWidget {
), ),
) )
.toList(), .toList(),
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh, getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
), ),
titlesData: FlTitlesData( titlesData: FlTitlesData(

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
@ -41,6 +42,7 @@ class _ChatScreenState extends State<ChatScreen> {
Future<void> _fetchWhatsNew() async { Future<void> _fetchWhatsNew() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/whats-new'); final resp = await sn.client.get('/cgi/im/whats-new');
if (resp.data == null) return;
final List<dynamic> out = resp.data; final List<dynamic> out = resp.data;
setState(() { setState(() {
_unreadCounts = {for (var v in out) v['channel_id']: v['count']}; _unreadCounts = {for (var v in out) v['channel_id']: v['count']};
@ -72,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> {
if (!mounted) return; if (!mounted) return;
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
final idSet = <int>{};
for (final channel in channels) { for (final channel in channels) {
if (channel.type == 1) { if (channel.type == 1) {
await ud.listAccount( idSet.addAll(
channel.members channel.members
?.cast<SnChannelMember?>() ?.cast<SnChannelMember?>()
.map((ele) => ele?.accountId) .map((ele) => ele?.accountId)
.where((ele) => ele != null) .where((ele) => ele != null)
.toSet() ?? .cast<int>() ??
{}, [],
); );
} }
} }
if (idSet.isNotEmpty) await ud.listAccount(idSet);
if (mounted) setState(() => _channels = channels); if (mounted) setState(() => _channels = channels);
}) })
@ -135,6 +139,28 @@ class _ChatScreenState extends State<ChatScreen> {
_fetchWhatsNew(); _fetchWhatsNew();
} }
void _onTapChannel(SnChannel channel) {
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
if (doExpand) {
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) {
_unreadCounts?[channel.id] = 0;
setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true);
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
@ -258,17 +284,42 @@ class _ChatScreenState extends State<ChatScreen> {
channel.name), channel.name),
), ),
const Gap(8), const Gap(8),
if (_unreadCounts?[channel.id] != null) if (_unreadCounts?[channel.id] != null &&
_unreadCounts![channel.id]! > 0)
Badge( Badge(
label: Text('${_unreadCounts![channel.id]}'), label: Text('${_unreadCounts![channel.id]}'),
), ),
], ],
), ),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Row(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', children: [
maxLines: 1, Expanded(
overflow: TextOverflow.ellipsis, child: Text(
lastMessage.body['text'] ??
'Unable preview',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(4),
Text(
DateFormat(
lastMessage.createdAt.toLocal().day ==
DateTime.now().day
? 'HH:mm'
: lastMessage.createdAt
.toLocal()
.year ==
DateTime.now().year
? 'MM/dd'
: 'yy/MM/dd',
).format(lastMessage.createdAt.toLocal()),
style: GoogleFonts.robotoMono(
fontSize: 12,
),
),
],
) )
: Text( : Text(
channel.description, channel.description,
@ -283,19 +334,7 @@ class _ChatScreenState extends State<ChatScreen> {
?.avatar, ?.avatar,
), ),
onTap: () { onTap: () {
if (doExpand) { _onTapChannel(channel);
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels(noRemote: true);
});
}, },
); );
} }
@ -305,17 +344,54 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
Expanded(child: Text(channel.name)), Expanded(child: Text(channel.name)),
const Gap(8), const Gap(8),
if (_unreadCounts?[channel.id] != null) if (_unreadCounts?[channel.id] != null &&
_unreadCounts![channel.id]! > 0)
Badge( Badge(
label: Text('${_unreadCounts![channel.id]}'), label: Text('${_unreadCounts![channel.id]}'),
), ),
], ],
), ),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Row(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', children: [
maxLines: 1, Badge(
overflow: TextOverflow.ellipsis, label: Text(ud
.getAccountFromCache(
lastMessage.sender.accountId)
?.nick ??
'unknown'.tr()),
backgroundColor:
Theme.of(context).colorScheme.primary,
textColor:
Theme.of(context).colorScheme.onPrimary,
),
const Gap(6),
Expanded(
child: Text(
lastMessage.body['text'] ??
'Unable preview',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(4),
Text(
DateFormat(
lastMessage.createdAt.toLocal().day ==
DateTime.now().day
? 'HH:mm'
: lastMessage.createdAt
.toLocal()
.year ==
DateTime.now().year
? 'MM/dd'
: 'yy/MM/dd',
).format(lastMessage.createdAt.toLocal()),
style: GoogleFonts.robotoMono(
fontSize: 12,
),
),
],
) )
: Text( : Text(
channel.description, channel.description,
@ -325,23 +401,16 @@ class _ChatScreenState extends State<ChatScreen> {
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: null, content: channel.realm?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20), fallbackWidget: const Icon(Symbols.chat, size: 20),
), ),
onTap: () { onTap: () {
if (doExpand) { if (doExpand) {
_unreadCounts?[channel.id] = 0;
setState(() => _focusChannel = channel); setState(() => _focusChannel = channel);
return; return;
} }
GoRouter.of(context).pushNamed( _onTapChannel(channel);
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels(noRemote: true);
});
}, },
); );
}, },

View File

@ -13,6 +13,7 @@ import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
@ -84,6 +85,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
orElse: () => null, orElse: () => null,
); );
} }
if (!mounted) return;
final nty = context.read<NotificationProvider>();
nty.skippableNotifyChannel = _channel!.id;
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -232,6 +237,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
void dispose() { void dispose() {
_wsSubscription?.cancel(); _wsSubscription?.cancel();
_messageController.dispose(); _messageController.dispose();
final nty = context.read<NotificationProvider>();
nty.skippableNotifyChannel = null;
super.dispose(); super.dispose();
} }

View File

@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -94,8 +93,12 @@ class _HomeScreenState extends State<HomeScreen> {
: MainAxisAlignment.start, : MainAxisAlignment.start,
children: [ children: [
_HomeDashUpdateWidget( _HomeDashUpdateWidget(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 8, left: 8, right: 8)), bottom: 8,
left: 8,
right: 8,
),
),
_HomeDashSpecialDayWidget().padding(horizontal: 8), _HomeDashSpecialDayWidget().padding(horizontal: 8),
StaggeredGrid.extent( StaggeredGrid.extent(
maxCrossAxisExtent: 280, maxCrossAxisExtent: 280,

View File

@ -29,6 +29,7 @@ const Map<String, IconData> kNotificationTopicIcons = {
'passport.security.otp': Symbols.password, 'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions, 'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction, 'interactive.feedback': Symbols.add_reaction,
'interactive.reply': Symbols.reply,
'messaging.callStart': Symbols.call_received, 'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt, 'wallet.transaction.new': Symbols.receipt,
}; };
@ -57,10 +58,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>(); final nty = context.read<NotificationProvider>();
final resp = await sn.client.get('/cgi/id/notifications?take=10'); final resp =
await sn.client.get('/cgi/id/notifications', queryParameters: {
'take': 10,
'offset': _notifications.length,
});
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_notifications.addAll( _notifications.addAll(
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [], resp.data['data']
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[],
); );
nty.updateTray(); nty.updateTray();
} catch (err) { } catch (err) {
@ -186,7 +194,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications(); _fetchNotifications();
}, },
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!, hasReachedMax: _totalCount != null &&
_notifications.length >= _totalCount!,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final nty = _notifications[idx]; final nty = _notifications[idx];
return Row( return Row(
@ -218,13 +227,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
isAutoWarp: true, isAutoWarp: true,
), ),
), ),
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription'] if ([
.contains(nty.topic) && 'interactive.reply',
'interactive.feedback',
'interactive.subscription'
].contains(nty.topic) &&
nty.metadata['related_post'] != null) nty.metadata['related_post'] != null)
GestureDetector( GestureDetector(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(
Radius.circular(8)),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -243,7 +256,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postDetail', 'postDetail',
pathParameters: { pathParameters: {
'slug': nty.metadata['related_post']!['id'].toString(), 'slug': nty
.metadata['related_post']!['id']
.toString(),
}, },
); );
}, },
@ -272,8 +287,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton( IconButton(
icon: const Icon(Symbols.check), icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), visualDensity:
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty), const VisualDensity(horizontal: -4, vertical: -4),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
), ),
], ],
).padding(horizontal: 16); ).padding(horizontal: 16);

View File

@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
child: InfiniteList( child: InfiniteList(
itemCount: _packs.length, itemCount: _packs.length,
onFetchData: _fetchPacks, onFetchData: _fetchPacks,
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!, hasReachedMax:
(_totalCount != null && _packs.length >= _totalCount!) ||
_tabController.index == 2,
isLoading: _isBusy, isLoading: _isBusy,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final pack = _packs[idx]; final pack = _packs[idx];

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
@ -11,7 +12,8 @@ class ThemeSet {
ThemeSet({required this.light, required this.dark}); ThemeSet({required this.light, required this.dark});
} }
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async { Future<ThemeSet> createAppThemeSet(
{Color? seedColorOverride, bool? useMaterial3}) async {
return ThemeSet( return ThemeSet(
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
@ -26,20 +28,24 @@ Future<ThemeData> createAppTheme(
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey); final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo; final seedColor =
seedColorString != null ? Color(seedColorString) : Colors.indigo;
final colorScheme = ColorScheme.fromSeed( final colorScheme = ColorScheme.fromSeed(
seedColor: seedColorOverride ?? seedColor, seedColor: seedColorOverride ?? seedColor,
brightness: brightness, brightness: brightness,
); );
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarTransparent =
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 =
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
return ThemeData( return ThemeData(
useMaterial3: useM3, useMaterial3: useM3,
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
textTheme: GoogleFonts.rubikTextTheme(),
iconTheme: IconThemeData( iconTheme: IconThemeData(
fill: 0, fill: 0,
weight: 400, weight: 400,
@ -52,8 +58,10 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarTransparent ? 0 : null, elevation: hasAppBarTransparent ? 0 : null,
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary, backgroundColor:
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor:
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
pageTransitionsTheme: PageTransitionsTheme( pageTransitionsTheme: PageTransitionsTheme(
builders: { builders: {

View File

@ -10,6 +10,7 @@ class AccountImage extends StatelessWidget {
final Color? foregroundColor; final Color? foregroundColor;
final double? radius; final double? radius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
final Widget? badge;
const AccountImage({ const AccountImage({
super.key, super.key,
@ -18,6 +19,7 @@ class AccountImage extends StatelessWidget {
this.foregroundColor, this.foregroundColor,
this.radius, this.radius,
this.fallbackWidget, this.fallbackWidget,
this.badge,
}); });
@override @override
@ -26,26 +28,36 @@ class AccountImage extends StatelessWidget {
final url = sn.getAttachmentUrl(content ?? ''); final url = sn.getAttachmentUrl(content ?? '');
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return CircleAvatar( return Stack(
key: Key('attachment-${content.hashCode}'), children: [
radius: radius, CircleAvatar(
backgroundColor: backgroundColor, key: Key('attachment-${content.hashCode}'),
backgroundImage: (content?.isNotEmpty ?? false) radius: radius,
? ResizeImage( backgroundColor: backgroundColor,
UniversalImage.provider(url), backgroundImage: (content?.isNotEmpty ?? false)
width: ((radius ?? 20) * devicePixelRatio * 2).round(), ? ResizeImage(
height: ((radius ?? 20) * devicePixelRatio * 2).round(), UniversalImage.provider(url),
policy: ResizeImagePolicy.fit, width: ((radius ?? 20) * devicePixelRatio * 2).round(),
) height: ((radius ?? 20) * devicePixelRatio * 2).round(),
: null, policy: ResizeImagePolicy.fit,
child: (content?.isEmpty ?? true) )
? (fallbackWidget ?? : null,
Icon( child: (content?.isEmpty ?? true)
Symbols.account_circle, ? (fallbackWidget ??
size: radius != null ? radius! * 1.2 : 24, Icon(
color: foregroundColor, Symbols.account_circle,
)) size: radius != null ? radius! * 1.2 : 24,
: null, color: foregroundColor,
))
: null,
),
if (badge != null)
Positioned(
right: -4,
bottom: -4,
child: badge!,
),
],
); );
} }
} }

View File

@ -11,6 +11,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart'; import 'package:surface/widgets/account/account_popover.dart';
@ -105,6 +106,22 @@ class ChatMessage extends StatelessWidget {
GestureDetector( GestureDetector(
child: AccountImage( child: AccountImage(
content: user?.avatar, content: user?.avatar,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
)
: null,
), ),
onTap: () { onTap: () {
if (user == null) return; if (user == null) return;
@ -161,7 +178,7 @@ class ChatMessage extends StatelessWidget {
if (data.preload?.quoteEvent != null) if (data.preload?.quoteEvent != null)
StyledWidget(Container( StyledWidget(Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: 480, maxWidth: 360,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
@ -210,9 +227,8 @@ class ChatMessage extends StatelessWidget {
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
maxHeight: 560, maxHeight: 360,
maxWidth: 480, maxWidth: 480 - 48 - padding.left,
minWidth: 480,
padding: padding.copyWith(top: 8, left: 48 + padding.left), padding: padding.copyWith(top: 8, left: 48 + padding.left),
), ),
if (!hasMerged && !isCompact) if (!hasMerged && !isCompact)
@ -292,14 +308,11 @@ class _ChatMessageText extends StatelessWidget {
buttonItems: items, buttonItems: items,
); );
}, },
child: Container( child: MarkdownTextContent(
constraints: const BoxConstraints(maxWidth: 480), content: data.body['text'],
child: MarkdownTextContent( isAutoWarp: true,
content: data.body['text'], isEnlargeSticker:
isAutoWarp: true, RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
),
), ),
), ),
if (data.updatedAt != data.createdAt) if (data.updatedAt != data.createdAt)

View File

@ -1,6 +1,9 @@
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
@ -72,21 +75,27 @@ class MarkdownTextContent extends StatelessWidget {
), ),
code: GoogleFonts.robotoMono(height: 1), code: GoogleFonts.robotoMono(height: 1),
), ),
builders: {}, builders: {
'latex': LatexElementBuilder(),
'code': HighlightBuilder(),
},
softLineBreak: true, softLineBreak: true,
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
markdown.CodeBlockSyntax(),
markdown.FencedCodeBlockSyntax(),
LatexBlockSyntax(),
], ],
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
if (isAutoWarp) markdown.LineBreakSyntax(), if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(), _UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context), _CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(), markdown.CodeSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes LatexInlineSyntax(),
], ],
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
@ -260,3 +269,56 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
return true; return true;
} }
} }
class HighlightBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfterWithContext(
BuildContext context,
markdown.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final isDark = Theme.of(context).brightness == Brightness.dark;
if (element.attributes['class'] == null &&
!element.textContent.trim().contains('\n')) {
return Container(
padding:
EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0),
margin: EdgeInsets.symmetric(horizontal: 2.0),
color: Colors.black12,
child: Text(
element.textContent,
style: GoogleFonts.robotoMono(textStyle: preferredStyle),
));
} else {
var language = 'plaintext';
final pattern = RegExp(r'^language-(.+)$');
if (element.attributes['class'] != null &&
pattern.hasMatch(element.attributes['class'] ?? '')) {
language =
pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ??
'plaintext';
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: HighlightView(
element.textContent.trim(),
language: language,
theme: {
...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!),
'root': (isDark
? TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xfff8f8f2))
: TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xff545454)))
},
padding: EdgeInsets.all(12),
textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle),
),
);
}
}
}

View File

@ -188,29 +188,19 @@ class AppRootScaffold extends StatelessWidget {
child: Text( child: Text(
'Solar Network', 'Solar Network',
style: GoogleFonts.spaceGrotesk(), style: GoogleFonts.spaceGrotesk(),
textAlign: !kIsWeb textAlign: Platform.isMacOS
? Platform.isMacOS ? TextAlign.center
? TextAlign.center : TextAlign.start,
: null
: null,
).padding(horizontal: 12, vertical: 5), ).padding(horizontal: 12, vertical: 5),
), ),
if (!Platform.isMacOS) if (!Platform.isMacOS)
Row( MinimizeWindowButton(colors: windowButtonColor),
mainAxisSize: MainAxisSize.min, if (!Platform.isMacOS)
children: [ MaximizeWindowButton(colors: windowButtonColor),
Expanded(child: MoveWindow()), if (!Platform.isMacOS)
Row( CloseWindowButton(
children: [ colors: windowButtonColor,
MinimizeWindowButton( onPressed: () => appWindow.hide(),
colors: windowButtonColor),
MaximizeWindowButton(
colors: windowButtonColor),
CloseWindowButton(
colors: windowButtonColor),
],
),
],
), ),
], ],
), ),
@ -226,16 +216,18 @@ class AppRootScaffold extends StatelessWidget {
child: NotifyIndicator()), child: NotifyIndicator()),
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
Positioned( Positioned(
bottom: safeBottom > 0 ? safeBottom : 16, bottom: safeBottom > 0 ? safeBottom : 16,
left: 0, left: 0,
right: 0, right: 0,
child: ConnectionIndicator()) child: ConnectionIndicator(),
)
else else
Positioned( Positioned(
top: safeTop > 0 ? safeTop : 16, top: safeTop > 0 ? safeTop : 16,
left: 0, left: 0,
right: 0, right: 0,
child: ConnectionIndicator()), child: ConnectionIndicator(),
),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,

View File

@ -193,7 +193,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.49.0) - sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
@ -378,7 +378,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f

View File

@ -405,10 +405,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -525,10 +525,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c" sha256: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.0.1"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@ -710,6 +710,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
flutter_highlight:
dependency: "direct main"
description:
name: flutter_highlight
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
@ -803,6 +811,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6+2" version: "0.7.6+2"
flutter_markdown_latex:
dependency: "direct main"
description:
name: flutter_markdown_latex
sha256: "839e76a84abb3632ffcebbd450cf93c7e9894af65622527d23f0084cee1bfd04"
url: "https://pub.dev"
source: hosted
version: "0.3.4"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -941,6 +965,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
highlight:
dependency: transitive
description:
name: highlight
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
home_widget: home_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1393,18 +1425,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.1" version: "8.3.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.2.0"
pasteboard: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1950,10 +1982,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233" sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.30" version: "0.5.31"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
@ -2050,6 +2082,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.3.2"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.3.2+72 version: 2.3.2+75
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -128,6 +128,8 @@ dependencies:
drift: ^2.25.1 drift: ^2.25.1
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
local_notifier: ^0.1.6 local_notifier: ^0.1.6
flutter_markdown_latex: ^0.3.4
flutter_highlight: ^0.7.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: