Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae9a7eb0fd | |||
5d6fb2442f | |||
5a85985534 | |||
c80499db03 | |||
b8dcdb2315 | |||
b7b921f1f4 | |||
319d5c7d7f | |||
4b5b001739 |
@ -26,7 +26,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
26
api/Passport/Developer Notify One User.bru
Normal file
26
api/Passport/Developer Notify One User.bru
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Developer Notify One User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "测试",
|
||||||
|
"subtitle": "Alphabot です",
|
||||||
|
"content": "全新通知动画",
|
||||||
|
"metadata": {
|
||||||
|
"image": "D2EDbcrsTugs3xk5"
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
@ -241,6 +241,8 @@
|
|||||||
"settingsMisc": "Misc",
|
"settingsMisc": "Misc",
|
||||||
"settingsMiscAbout": "About",
|
"settingsMiscAbout": "About",
|
||||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||||
|
"settingsAccountLanguage": "Account Language",
|
||||||
|
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
|
||||||
"sensitiveContent": "Sensitive Content",
|
"sensitiveContent": "Sensitive Content",
|
||||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||||
@ -562,6 +564,7 @@
|
|||||||
"shareIntent": "Share",
|
"shareIntent": "Share",
|
||||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||||
"shareIntentPostStory": "Post a Story",
|
"shareIntentPostStory": "Post a Story",
|
||||||
|
"shareIntentSendChannel": "Share to Channel",
|
||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"updateOngoing": "Updating, please wait...",
|
"updateOngoing": "Updating, please wait...",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
@ -574,6 +577,7 @@
|
|||||||
"colorSchemeWhite": "White",
|
"colorSchemeWhite": "White",
|
||||||
"colorSchemeBlack": "Black",
|
"colorSchemeBlack": "Black",
|
||||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||||
|
"postFeaturedComment": "Featured Comment",
|
||||||
"postCategoryTechnology": "Technology",
|
"postCategoryTechnology": "Technology",
|
||||||
"postCategoryGaming": "Gaming",
|
"postCategoryGaming": "Gaming",
|
||||||
"postCategoryLife": "Life",
|
"postCategoryLife": "Life",
|
||||||
@ -604,5 +608,6 @@
|
|||||||
"one": "{} Source Point",
|
"one": "{} Source Point",
|
||||||
"other": "{} Source Points"
|
"other": "{} Source Points"
|
||||||
},
|
},
|
||||||
"aiThinkingProcess": "AI Thinking Process"
|
"aiThinkingProcess": "AI Thinking Process",
|
||||||
|
"accountSettingsApplied": "Account settings have been applied."
|
||||||
}
|
}
|
||||||
|
@ -239,6 +239,8 @@
|
|||||||
"settingsMisc": "杂项",
|
"settingsMisc": "杂项",
|
||||||
"settingsMiscAbout": "关于",
|
"settingsMiscAbout": "关于",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帐号偏好语言",
|
||||||
|
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||||
"sensitiveContent": "敏感内容",
|
"sensitiveContent": "敏感内容",
|
||||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||||
@ -560,6 +562,7 @@
|
|||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||||
"shareIntentPostStory": "发布动态",
|
"shareIntentPostStory": "发布动态",
|
||||||
|
"shareIntentSendChannel": "分享到聊天频道",
|
||||||
"updateAvailable": "检测到更新可用",
|
"updateAvailable": "检测到更新可用",
|
||||||
"updateOngoing": "正在更新,请稍后……",
|
"updateOngoing": "正在更新,请稍后……",
|
||||||
"custom": "自定义",
|
"custom": "自定义",
|
||||||
@ -572,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||||
|
"postFeaturedComment": "精选评论",
|
||||||
"postCategoryTechnology": "技术",
|
"postCategoryTechnology": "技术",
|
||||||
"postCategoryGaming": "游戏",
|
"postCategoryGaming": "游戏",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -602,5 +606,6 @@
|
|||||||
"one": "{} 源点",
|
"one": "{} 源点",
|
||||||
"other": "{} 源点"
|
"other": "{} 源点"
|
||||||
},
|
},
|
||||||
"aiThinkingProcess": "AI 思考过程"
|
"aiThinkingProcess": "AI 思考过程",
|
||||||
|
"accountSettingsApplied": "帐号设置已应用。"
|
||||||
}
|
}
|
||||||
|
@ -239,6 +239,8 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
@ -560,6 +562,7 @@
|
|||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@ -572,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -602,5 +606,6 @@
|
|||||||
"one": "{} 源點",
|
"one": "{} 源點",
|
||||||
"other": "{} 源點"
|
"other": "{} 源點"
|
||||||
},
|
},
|
||||||
"aiThinkingProcess": "AI 思考過程"
|
"aiThinkingProcess": "AI 思考過程",
|
||||||
|
"accountSettingsApplied": "帳號設置已應用。"
|
||||||
}
|
}
|
||||||
|
@ -239,6 +239,8 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
@ -560,6 +562,7 @@
|
|||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@ -572,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@ -602,5 +606,6 @@
|
|||||||
"one": "{} 源點",
|
"one": "{} 源點",
|
||||||
"other": "{} 源點"
|
"other": "{} 源點"
|
||||||
},
|
},
|
||||||
"aiThinkingProcess": "AI 思考過程"
|
"aiThinkingProcess": "AI 思考過程",
|
||||||
|
"accountSettingsApplied": "帳號設置已應用。"
|
||||||
}
|
}
|
||||||
|
@ -71,22 +71,29 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int showingCount = 0;
|
||||||
List<SnNotification> notifications = List.empty(growable: true);
|
List<SnNotification> notifications = List.empty(growable: true);
|
||||||
|
|
||||||
void listen() {
|
void listen() {
|
||||||
_ws.stream.stream.listen((event) {
|
_ws.stream.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!);
|
||||||
|
if (showingCount < 0) showingCount = 0;
|
||||||
|
showingCount++;
|
||||||
notifications.add(notification);
|
notifications.add(notification);
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
if (showingCount >= 0) showingCount--;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
if (doHaptic) HapticFeedback.lightImpact();
|
if (doHaptic) HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
notifications.clear();
|
showingCount = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
|
|||||||
user = null;
|
user = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLanguage(String? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
if (user == null) return;
|
||||||
|
user = user!.copyWith(language: value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ final _appRoutes = [
|
|||||||
postRepostId: int.tryParse(
|
postRepostId: int.tryParse(
|
||||||
state.uri.queryParameters['reposting'] ?? '',
|
state.uri.queryParameters['reposting'] ?? '',
|
||||||
),
|
),
|
||||||
extraProps: state.extra as PostEditorExtraProps?,
|
extraProps: state.extra as PostEditorExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@ -156,6 +156,7 @@ final _appRoutes = [
|
|||||||
builder: (context, state) => ChatRoomScreen(
|
builder: (context, state) => ChatRoomScreen(
|
||||||
scope: state.pathParameters['scope']!,
|
scope: state.pathParameters['scope']!,
|
||||||
alias: state.pathParameters['alias']!,
|
alias: state.pathParameters['alias']!,
|
||||||
|
extra: state.extra as ChatRoomScreenExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenAccount").tr(),
|
title: Text(
|
||||||
|
"screenAccount",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||||
? Stack(
|
? Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
@ -1,17 +1,41 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.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:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:intl/locale.dart';
|
||||||
|
|
||||||
class AccountSettingsScreen extends StatelessWidget {
|
class AccountSettingsScreen extends StatelessWidget {
|
||||||
const AccountSettingsScreen({super.key});
|
const AccountSettingsScreen({super.key});
|
||||||
|
|
||||||
|
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
await sn.client.put('/cgi/id/users/me/language', data: {
|
||||||
|
'language': value.toString(),
|
||||||
|
});
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showSnackbar('accountSettingsApplied'.tr());
|
||||||
|
await ua.refreshUser();
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.watch<UserProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: PageBackButton(),
|
leading: PageBackButton(),
|
||||||
@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsAccountLanguage').tr(),
|
||||||
|
subtitle: Text('settingsAccountLanguageDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
leading: const Icon(Symbols.translate),
|
||||||
|
trailing: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Locale?>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||||
|
return DropdownMenuItem<Locale?>(
|
||||||
|
value: Locale.parse(ele.toString()),
|
||||||
|
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
||||||
|
onChanged: (Locale? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
_setAccountLanguage(context, value);
|
||||||
|
ua.setLanguage(value.toString());
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 160,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('accountProfileEdit').tr(),
|
title: Text('accountProfileEdit').tr(),
|
||||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||||
|
@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'nick': nickname,
|
'nick': nickname,
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
|
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_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/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||||
@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart';
|
|||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/user_directory.dart';
|
class ChatRoomScreenExtra {
|
||||||
import '../../providers/userinfo.dart';
|
final String? initialText;
|
||||||
|
final List<PostWriteMedia>? initialAttachments;
|
||||||
|
|
||||||
|
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||||
|
}
|
||||||
|
|
||||||
class ChatRoomScreen extends StatefulWidget {
|
class ChatRoomScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
final ChatRoomScreenExtra? extra;
|
||||||
|
|
||||||
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||||
@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_messageController = ChatMessageController(context);
|
_messageController = ChatMessageController(context);
|
||||||
_fetchChannel().then((_) async {
|
_fetchChannel().then((_) async {
|
||||||
await _messageController.initialize(_channel!);
|
await _messageController.initialize(_channel!);
|
||||||
await _messageController.checkUpdate();
|
|
||||||
await _fetchOngoingCall();
|
if (widget.extra != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
log('[ChatInput] Setting initial text and attachments...');
|
||||||
|
if (widget.extra!.initialText != null) {
|
||||||
|
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
|
||||||
|
}
|
||||||
|
if (widget.extra!.initialAttachments != null) {
|
||||||
|
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
_messageController.checkUpdate(),
|
||||||
|
_fetchOngoingCall(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
|
@ -288,6 +288,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
@ -21,6 +21,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
import '../widgets/unauthorized_hint.dart';
|
import '../widgets/unauthorized_hint.dart';
|
||||||
|
|
||||||
|
const Map<String, IconData> kNotificationTopicIcons = {
|
||||||
|
'general': Symbols.notifications,
|
||||||
|
'passport.security.alert': Symbols.gpp_maybe,
|
||||||
|
'passport.security.otp': Symbols.password,
|
||||||
|
'interactive.subscription': Symbols.subscriptions,
|
||||||
|
'interactive.feedback': Symbols.add_reaction,
|
||||||
|
'messaging.callStart': Symbols.call_received,
|
||||||
|
'wallet.transaction.new': Symbols.receipt,
|
||||||
|
};
|
||||||
|
|
||||||
class NotificationScreen extends StatefulWidget {
|
class NotificationScreen extends StatefulWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationScreen({super.key});
|
||||||
|
|
||||||
@ -36,15 +46,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
|
|
||||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
|
||||||
'passport.security.alert': Symbols.gpp_maybe,
|
|
||||||
'passport.security.otp': Symbols.password,
|
|
||||||
'interactive.subscription': Symbols.subscriptions,
|
|
||||||
'interactive.feedback': Symbols.add_reaction,
|
|
||||||
'messaging.callStart': Symbols.call_received,
|
|
||||||
'wallet.transaction.new': Symbols.receipt,
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<void> _fetchNotifications() async {
|
Future<void> _fetchNotifications() async {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) return;
|
if (!ua.isAuthorized) return;
|
||||||
|
@ -20,13 +20,13 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
|
|||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class PostEditorExtraProps {
|
class PostEditorExtra {
|
||||||
final String? text;
|
final String? text;
|
||||||
final String? title;
|
final String? title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final List<PostWriteMedia>? attachments;
|
final List<PostWriteMedia>? attachments;
|
||||||
|
|
||||||
const PostEditorExtraProps({
|
const PostEditorExtra({
|
||||||
this.text,
|
this.text,
|
||||||
this.title,
|
this.title,
|
||||||
this.description,
|
this.description,
|
||||||
@ -39,7 +39,7 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
final int? postEditId;
|
final int? postEditId;
|
||||||
final int? postReplyId;
|
final int? postReplyId;
|
||||||
final int? postRepostId;
|
final int? postRepostId;
|
||||||
final PostEditorExtraProps? extraProps;
|
final PostEditorExtra? extraProps;
|
||||||
|
|
||||||
const PostEditorScreen({
|
const PostEditorScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.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/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/chat/room.dart';
|
||||||
import 'package:surface/screens/post/post_editor.dart';
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class AppSharingListener extends StatefulWidget {
|
class AppSharingListener extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
pathParameters: {
|
pathParameters: {
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
extra: PostEditorExtraProps(
|
extra: PostEditorExtra(
|
||||||
text: value
|
text: value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||||
.map((e) => e.path).join('\n'),
|
.map((e) => e.path)
|
||||||
|
.join('\n'),
|
||||||
attachments: value
|
attachments: value
|
||||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
.contains(e.type))
|
||||||
|
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
leading: Icon(Icons.chat_outlined),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
title: Text('shareIntentSendChannel').tr(),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||||
|
).then((val) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (val == true) Navigator.pop(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
).width(280),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
return widget.child;
|
return widget.child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ShareIntentChannelSelect extends StatefulWidget {
|
||||||
|
final Iterable<SharedMediaFile> value;
|
||||||
|
|
||||||
|
const _ShareIntentChannelSelect({required this.value});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
List<SnChannel>? _channels;
|
||||||
|
Map<int, SnChatMessage>? _lastMessages;
|
||||||
|
|
||||||
|
void _refreshChannels() {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final chan = context.read<ChatChannelProvider>();
|
||||||
|
chan.fetchChannels().listen((channels) async {
|
||||||
|
final lastMessages = await chan.getLastMessages(channels);
|
||||||
|
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||||
|
channels.sort((a, b) {
|
||||||
|
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
||||||
|
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
||||||
|
}
|
||||||
|
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||||
|
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
for (final channel in channels) {
|
||||||
|
if (channel.type == 1) {
|
||||||
|
await ud.listAccount(
|
||||||
|
channel.members
|
||||||
|
?.cast<SnChannelMember?>()
|
||||||
|
.map((ele) => ele?.accountId)
|
||||||
|
.where((ele) => ele != null)
|
||||||
|
.toSet() ??
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) setState(() => _channels = channels);
|
||||||
|
})
|
||||||
|
..onError((err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
})
|
||||||
|
..onDone(() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_refreshChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.chat, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Expanded(
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final channel = _channels![idx];
|
||||||
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
|
if (channel.type == 1) {
|
||||||
|
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Text(
|
||||||
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'channelDirectMessageDescription'.tr(args: [
|
||||||
|
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||||
|
]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted) _refreshChannels();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(channel.name),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Text(
|
||||||
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
channel.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
extra: ChatRoomScreenExtra(
|
||||||
|
initialText: widget.value
|
||||||
|
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||||
|
.map((e) => e.path)
|
||||||
|
.join('\n'),
|
||||||
|
initialAttachments: widget.value
|
||||||
|
.where((e) =>
|
||||||
|
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||||
|
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
if (value == true) _refreshChannels();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ class SnAccount with _$SnAccount {
|
|||||||
required String name,
|
required String name,
|
||||||
required String nick,
|
required String nick,
|
||||||
required Map<String, dynamic> permNodes,
|
required Map<String, dynamic> permNodes,
|
||||||
|
required String language,
|
||||||
required SnAccountProfile? profile,
|
required SnAccountProfile? profile,
|
||||||
@Default([]) List<SnAccountBadge> badges,
|
@Default([]) List<SnAccountBadge> badges,
|
||||||
required DateTime? suspendedAt,
|
required DateTime? suspendedAt,
|
||||||
|
@ -33,6 +33,7 @@ mixin _$SnAccount {
|
|||||||
String get name => throw _privateConstructorUsedError;
|
String get name => throw _privateConstructorUsedError;
|
||||||
String get nick => throw _privateConstructorUsedError;
|
String get nick => throw _privateConstructorUsedError;
|
||||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||||
|
String get language => throw _privateConstructorUsedError;
|
||||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||||
@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
? _value.permNodes
|
? _value.permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
? _value._permNodes
|
? _value._permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@ -373,6 +386,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.nick,
|
required this.nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
required final Map<String, dynamic> permNodes,
|
||||||
|
required this.language,
|
||||||
required this.profile,
|
required this.profile,
|
||||||
final List<SnAccountBadge> badges = const [],
|
final List<SnAccountBadge> badges = const [],
|
||||||
required this.suspendedAt,
|
required this.suspendedAt,
|
||||||
@ -429,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
return EqualUnmodifiableMapView(_permNodes);
|
return EqualUnmodifiableMapView(_permNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String language;
|
||||||
@override
|
@override
|
||||||
final SnAccountProfile? profile;
|
final SnAccountProfile? profile;
|
||||||
final List<SnAccountBadge> _badges;
|
final List<SnAccountBadge> _badges;
|
||||||
@ -453,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -479,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
(identical(other.nick, nick) || other.nick == nick) &&
|
(identical(other.nick, nick) || other.nick == nick) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._permNodes, _permNodes) &&
|
.equals(other._permNodes, _permNodes) &&
|
||||||
|
(identical(other.language, language) ||
|
||||||
|
other.language == language) &&
|
||||||
(identical(other.profile, profile) || other.profile == profile) &&
|
(identical(other.profile, profile) || other.profile == profile) &&
|
||||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||||
(identical(other.suspendedAt, suspendedAt) ||
|
(identical(other.suspendedAt, suspendedAt) ||
|
||||||
@ -509,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
name,
|
name,
|
||||||
nick,
|
nick,
|
||||||
const DeepCollectionEquality().hash(_permNodes),
|
const DeepCollectionEquality().hash(_permNodes),
|
||||||
|
language,
|
||||||
profile,
|
profile,
|
||||||
const DeepCollectionEquality().hash(_badges),
|
const DeepCollectionEquality().hash(_badges),
|
||||||
suspendedAt,
|
suspendedAt,
|
||||||
@ -548,6 +567,7 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
required final String name,
|
required final String name,
|
||||||
required final String nick,
|
required final String nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
required final Map<String, dynamic> permNodes,
|
||||||
|
required final String language,
|
||||||
required final SnAccountProfile? profile,
|
required final SnAccountProfile? profile,
|
||||||
final List<SnAccountBadge> badges,
|
final List<SnAccountBadge> badges,
|
||||||
required final DateTime? suspendedAt,
|
required final DateTime? suspendedAt,
|
||||||
@ -586,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
@override
|
@override
|
||||||
Map<String, dynamic> get permNodes;
|
Map<String, dynamic> get permNodes;
|
||||||
@override
|
@override
|
||||||
|
String get language;
|
||||||
|
@override
|
||||||
SnAccountProfile? get profile;
|
SnAccountProfile? get profile;
|
||||||
@override
|
@override
|
||||||
List<SnAccountBadge> get badges;
|
List<SnAccountBadge> get badges;
|
||||||
|
@ -26,6 +26,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
|||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
nick: json['nick'] as String,
|
nick: json['nick'] as String,
|
||||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||||
|
language: json['language'] as String,
|
||||||
profile: json['profile'] == null
|
profile: json['profile'] == null
|
||||||
? null
|
? null
|
||||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||||
@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
|||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'nick': instance.nick,
|
'nick': instance.nick,
|
||||||
'perm_nodes': instance.permNodes,
|
'perm_nodes': instance.permNodes,
|
||||||
|
'language': instance.language,
|
||||||
'profile': instance.profile?.toJson(),
|
'profile': instance.profile?.toJson(),
|
||||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||||
|
@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Builder(builder: (context) {
|
Builder(builder: (context) {
|
||||||
|
@ -46,6 +46,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
setState(() => _replyingMessage = value);
|
setState(() => _replyingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setInitialText(String? value) {
|
||||||
|
_contentController.text = value ?? '';
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInitialAttachments(List<PostWriteMedia>? value) {
|
||||||
|
_attachments.addAll(value ?? []);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
void setEdit(SnChatMessage? value) {
|
void setEdit(SnChatMessage? value) {
|
||||||
_contentController.text = value?.body['text'] ?? '';
|
_contentController.text = value?.body['text'] ?? '';
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.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:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
|
|
||||||
@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ws = context.watch<WebSocketProvider>();
|
final ws = context.watch<WebSocketProvider>();
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
|
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: ws,
|
listenable: ws,
|
||||||
@ -22,6 +26,7 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !show,
|
ignoring: !show,
|
||||||
|
child: Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@ -29,13 +34,16 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
child: ua.isAuthorized
|
child: ua.isAuthorized
|
||||||
? Row(
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (ws.isBusy)
|
if (ws.isBusy)
|
||||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
else if (!ws.isConnected)
|
else if (!ws.isConnected)
|
||||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
Text('serverDisconnected')
|
||||||
|
.tr()
|
||||||
|
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
else
|
else
|
||||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -61,6 +69,7 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
).padding(left: marginLeft),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
// Leave padding for side navigation
|
// Leave padding for side navigation
|
||||||
mousePosition = cfg.drawerIsExpanded
|
mousePosition = cfg.drawerIsExpanded
|
||||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
final bool isAutoWarp;
|
final bool isAutoWarp;
|
||||||
final bool isEnlargeSticker;
|
final bool isEnlargeSticker;
|
||||||
final TextScaler? textScaler;
|
final TextScaler? textScaler;
|
||||||
|
final Color? textColor;
|
||||||
final List<SnAttachment?>? attachments;
|
final List<SnAttachment?>? attachments;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
this.isAutoWarp = false,
|
this.isAutoWarp = false,
|
||||||
this.isEnlargeSticker = false,
|
this.isEnlargeSticker = false,
|
||||||
this.textScaler,
|
this.textScaler,
|
||||||
|
this.textColor,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
textScaler: textScaler,
|
textScaler: textScaler,
|
||||||
|
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
|
||||||
blockquote: TextStyle(
|
blockquote: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
@ -31,7 +31,9 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||||
|
|
||||||
return NavigationRail(
|
return SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: NavigationRail(
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||||
destinations: [
|
destinations: [
|
||||||
@ -59,6 +61,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
nav.setIndex(idx);
|
nav.setIndex(idx);
|
||||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final safeTop = MediaQuery.of(context).padding.top;
|
final safeTop = MediaQuery.of(context).padding.top;
|
||||||
|
final safeBottom = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: globalRootScaffoldKey,
|
key: globalRootScaffoldKey,
|
||||||
@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
||||||
|
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
|
||||||
|
else
|
||||||
|
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||||
|
@ -1,60 +1,181 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/notification.dart';
|
||||||
|
import 'package:surface/types/notification.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class NotifyIndicator extends StatelessWidget {
|
import 'markdown_content.dart';
|
||||||
|
|
||||||
|
class NotifyIndicator extends StatefulWidget {
|
||||||
const NotifyIndicator({super.key});
|
const NotifyIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotifyIndicator> createState() => _NotifyIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _markOneAsRead(SnNotification notification) async {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) return;
|
||||||
|
|
||||||
|
if (notification.id == 0) return;
|
||||||
|
if (notification.readAt != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.put('/cgi/id/notifications/read/${notification.id}');
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar(
|
||||||
|
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final nty = context.watch<NotificationProvider>();
|
final nty = context.watch<NotificationProvider>();
|
||||||
|
|
||||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||||
|
|
||||||
|
final show = nty.showingCount > 0 && ua.isAuthorized;
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
_animationController.animateTo(1);
|
||||||
|
} else {
|
||||||
|
_animationController.animateTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nty,
|
listenable: nty,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
|
final current = nty.notifications.lastOrNull;
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !show,
|
ignoring: !show,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
child: Animate(
|
||||||
|
autoPlay: false,
|
||||||
|
controller: _animationController,
|
||||||
|
effects: [
|
||||||
|
SlideEffect(
|
||||||
|
begin: isMobile ? Offset(0, -1) : Offset(1, 0),
|
||||||
|
end: Offset(0, 0),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
),
|
||||||
|
FadeEffect(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 1.0,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360,
|
||||||
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: ua.isAuthorized
|
child: Row(
|
||||||
? Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
if (current?.metadata['avatar'] != null)
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundImage: UniversalImage.provider(
|
||||||
|
sn.getAttachmentUrl(current!.metadata['avatar']),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications),
|
||||||
|
const Gap(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
nty.notifications.lastOrNull?.title ??
|
current?.title ?? 'Notification',
|
||||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
maxLines: 1,
|
fontWeight: FontWeight.bold,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
if (nty.notifications.lastOrNull?.body != null)
|
),
|
||||||
|
if (current?.subtitle?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
nty.notifications.lastOrNull!.body,
|
current!.subtitle!,
|
||||||
maxLines: 1,
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
overflow: TextOverflow.ellipsis,
|
fontWeight: FontWeight.bold,
|
||||||
).padding(left: 4),
|
),
|
||||||
const Gap(8),
|
),
|
||||||
const Icon(Symbols.notifications_unread, size: 18),
|
MarkdownTextContent(
|
||||||
|
content: current?.body ?? '',
|
||||||
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 8, vertical: 4)
|
),
|
||||||
: const SizedBox.shrink(),
|
),
|
||||||
).opacity(show ? 1 : 0, animate: true).animate(
|
const Gap(16),
|
||||||
const Duration(milliseconds: 300),
|
Column(
|
||||||
Curves.easeInOut,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now()))
|
||||||
|
.fontSize(12)
|
||||||
|
.padding(right: 2),
|
||||||
|
const Gap(6),
|
||||||
|
if (current?.metadata['image'] != null)
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(current?.metadata['image']),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
nty.clear();
|
nty.clear();
|
||||||
|
if (current != null) {
|
||||||
|
_markOneAsRead(current);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@ -7,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.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:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@ -189,6 +189,7 @@ class PostItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
||||||
|
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
showComments: showComments,
|
||||||
@ -270,6 +271,7 @@ class PostItem extends StatelessWidget {
|
|||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
|
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -1125,6 +1127,95 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostFeaturedComment extends StatefulWidget {
|
||||||
|
final SnPost data;
|
||||||
|
final double? maxWidth;
|
||||||
|
|
||||||
|
const _PostFeaturedComment({required this.data, this.maxWidth});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PostFeaturedComment> createState() => _PostFeaturedCommentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||||
|
SnPost? _featuredComment;
|
||||||
|
|
||||||
|
Future<void> _fetchComments() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
||||||
|
'take': 1,
|
||||||
|
});
|
||||||
|
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.data.metric.replyCount > 0) {
|
||||||
|
_fetchComments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
|
||||||
|
if (_featuredComment == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return AnimateWidgetExtensions(Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||||
|
margin: const EdgeInsets.only(top: 8),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Material(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder: (context) => PostCommentListPopup(
|
||||||
|
postId: widget.data.id,
|
||||||
|
commentCount: widget.data.metric.replyCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 12,
|
||||||
|
backgroundImage: UniversalImage.provider(
|
||||||
|
_featuredComment!.publisher.avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
content: _featuredComment!.body['content'],
|
||||||
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _PostAbuseReportDialog extends StatefulWidget {
|
class _PostAbuseReportDialog extends StatefulWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.heavyImpact();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
|
@ -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.2.2+60
|
version: 2.2.2+61
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
|
Reference in New Issue
Block a user