diff --git a/assets/translations/en.json b/assets/translations/en.json
index 132be6d..ebc2ae5 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -228,5 +228,7 @@
"callVideoFlip": "Mirror video",
"callSpeakerphoneToggle": "Toggle speakerphone",
"callScreenOff": "Turn off screen share",
- "callScreenOn": "Turn on screen share"
+ "callScreenOn": "Turn on screen share",
+ "callMessageEnded": "Call lasted {}",
+ "callMessageStarted": "Call started"
}
diff --git a/assets/translations/zh.json b/assets/translations/zh.json
index cc3beae..8f6083b 100644
--- a/assets/translations/zh.json
+++ b/assets/translations/zh.json
@@ -228,5 +228,7 @@
"callVideoFlip": "镜像画面",
"callSpeakerphoneToggle": "切换扬声器",
"callScreenOff": "关闭屏幕共享",
- "callScreenOn": "开启屏幕共享"
+ "callScreenOn": "开启屏幕共享",
+ "callMessageEnded": "通话持续了 {}",
+ "callMessageStarted": "通话开始了"
}
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index d581ddd..bc9cc71 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -518,6 +518,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -703,6 +704,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -728,6 +730,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 36584e5..3b03791 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -45,6 +45,8 @@
fetch
remote-notification
+ audio
+ voip
UILaunchStoryboardName
LaunchScreen
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 903def2..29326e3 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -4,5 +4,7 @@
aps-environment
development
+ com.apple.developer.usernotifications.communication
+
diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart
index 2bb7843..a9bd1f3 100644
--- a/lib/screens/chat/call_room.dart
+++ b/lib/screens/chat/call_room.dart
@@ -35,7 +35,8 @@ class _CallRoomScreenState extends State {
return Stack(
children: [
Container(
- color: Theme.of(context).colorScheme.surfaceContainer,
+ color:
+ Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
child: call.focusTrack != null
? InteractiveParticipantWidget(
isFixedAvatar: false,
@@ -113,7 +114,10 @@ class _CallRoomScreenState extends State {
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ color: Theme.of(context)
+ .colorScheme
+ .surfaceContainerHigh
+ .withOpacity(0.75),
participant: track,
onTap: () {
if (track.participant.sid !=
diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart
index db0982d..4edee0c 100644
--- a/lib/screens/chat/room.dart
+++ b/lib/screens/chat/room.dart
@@ -138,6 +138,12 @@ class _ChatRoomScreenState extends State {
);
}
+ bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
+ if (a == null || b == null) return false;
+ if (a.sender.accountId != b.sender.accountId) return false;
+ return a.createdAt.difference(b.createdAt).inMinutes <= 3;
+ }
+
@override
void initState() {
super.initState();
@@ -248,27 +254,20 @@ class _ChatRoomScreenState extends State {
},
itemBuilder: (context, idx) {
final message = _messageController.messages[idx];
- final nextMessage =
- idx < _messageController.messages.length - 1
- ? _messageController.messages[idx + 1]
- : null;
- final previousMessage =
- idx > 0 ? _messageController.messages[idx - 1] : null;
- final canMerge = nextMessage != null &&
- nextMessage.senderId == message.senderId &&
- message.createdAt
- .difference(nextMessage.createdAt)
- .inMinutes
- .abs() <=
- 3;
- final canMergePrevious = previousMessage != null &&
- previousMessage.senderId == message.senderId &&
- message.createdAt
- .difference(previousMessage.createdAt)
- .inMinutes
- .abs() <=
- 3;
+ bool canMerge = false, canMergePrevious = false;
+ if (idx > 0) {
+ canMergePrevious = _checkMessageMergeable(
+ _messageController.messages[idx - 1],
+ _messageController.messages[idx],
+ );
+ }
+ if (idx + 1 < _messageController.messages.length) {
+ canMerge = _checkMessageMergeable(
+ _messageController.messages[idx],
+ _messageController.messages[idx + 1],
+ );
+ }
return ChatMessage(
data: message,
diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart
index f1e7606..23c1d5c 100644
--- a/lib/screens/post/post_detail.dart
+++ b/lib/screens/post/post_detail.dart
@@ -86,22 +86,28 @@ class _PostDetailScreenState extends State {
GoRouter.of(context).replaceNamed('explore');
},
),
- flexibleSpace: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (_data?.body['title'] != null)
- Text(_data?.body['title'] ?? 'postNoun'.tr())
- .textStyle(Theme.of(context).textTheme.titleLarge!)
- .textColor(Colors.white),
- if (_data?.body['title'] != null)
- Text('postDetail'.tr())
- .textColor(Colors.white.withAlpha((255 * 0.9).round()))
- else
- Text('postDetail'.tr())
- .textStyle(Theme.of(context).textTheme.titleLarge!)
- .textColor(Colors.white),
- ],
- ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
+ title: _data?.body['title'] != null
+ ? RichText(
+ textAlign: TextAlign.center,
+ text: TextSpan(children: [
+ TextSpan(
+ text: _data?.body['title'] ?? 'postNoun'.tr(),
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge!
+ .copyWith(color: Colors.white),
+ ),
+ const TextSpan(text: '\n'),
+ TextSpan(
+ text: 'postDetail'.tr(),
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall!
+ .copyWith(color: Colors.white),
+ ),
+ ]),
+ )
+ : Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart
index c9ebbc8..41fb1cd 100644
--- a/lib/screens/post/post_editor.dart
+++ b/lib/screens/post/post_editor.dart
@@ -1,5 +1,3 @@
-import 'dart:math' as math;
-
import 'package:collection/collection.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -129,18 +127,28 @@ class _PostEditorScreenState extends State {
Navigator.pop(context);
},
),
- flexibleSpace: Column(
- children: [
- Text(_writeController.title.isNotEmpty
- ? _writeController.title
- : 'untitled'.tr())
- .textStyle(Theme.of(context).textTheme.titleLarge!)
- .textColor(Colors.white),
- Text(PostWriteController.kTitleMap[widget.mode]!)
- .tr()
- .textColor(Colors.white.withAlpha((255 * 0.9).round())),
- ],
- ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
+ title: RichText(
+ textAlign: TextAlign.center,
+ text: TextSpan(children: [
+ TextSpan(
+ text: _writeController.title.isNotEmpty
+ ? _writeController.title
+ : 'untitled'.tr(),
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge!
+ .copyWith(color: Colors.white),
+ ),
+ const TextSpan(text: '\n'),
+ TextSpan(
+ text: PostWriteController.kTitleMap[widget.mode]!,
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall!
+ .copyWith(color: Colors.white),
+ ),
+ ]),
+ ),
actions: [
IconButton(
icon: const Icon(Symbols.tune),
diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart
index 09d771f..7d81dfc 100644
--- a/lib/widgets/chat/chat_message.dart
+++ b/lib/widgets/chat/chat_message.dart
@@ -47,8 +47,10 @@ class ChatMessage extends StatelessWidget {
return SwipeTo(
key: Key('chat-message-${data.id}'),
iconOnLeftSwipe: Symbols.reply,
+ iconOnRightSwipe: Symbols.edit,
swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
+ onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
child: ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
@@ -111,10 +113,6 @@ class ChatMessage extends StatelessWidget {
? data.sender.nick!
: user?.nick ?? 'unknown',
).bold(),
- if (data.updatedAt != data.createdAt)
- Text(
- 'messageEditedHint'.tr(),
- ).fontSize(14).opacity(0.75).padding(left: 6),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
@@ -163,7 +161,10 @@ class ChatMessage extends StatelessWidget {
maxHeight: 520,
listPadding: const EdgeInsets.only(top: 8),
),
- if (!hasMerged && !isCompact) const Gap(12),
+ if (!hasMerged && !isCompact)
+ const Gap(12)
+ else if (!isCompact)
+ const Gap(6),
],
),
),
@@ -178,9 +179,18 @@ class _ChatMessageText extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
- return MarkdownTextContent(
- content: data.body['text'],
- isAutoWarp: true,
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MarkdownTextContent(
+ content: data.body['text'],
+ isAutoWarp: true,
+ ),
+ if (data.updatedAt != data.createdAt)
+ Text(
+ 'messageEditedHint'.tr(),
+ ).fontSize(13).opacity(0.75),
+ ],
);
} else if (data.body['attachments']?.isNotEmpty) {
return Row(
@@ -204,6 +214,14 @@ class _ChatMessageSystemNotify extends StatelessWidget {
final SnChatMessage data;
const _ChatMessageSystemNotify({super.key, required this.data});
+ String _formatDuration(Duration duration) {
+ String negativeSign = duration.isNegative ? '-' : '';
+ String twoDigits(int n) => n.toString().padLeft(2, '0');
+ String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs());
+ String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
+ return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
+ }
+
@override
Widget build(BuildContext context) {
switch (data.type) {
@@ -227,6 +245,28 @@ class _ChatMessageSystemNotify extends StatelessWidget {
),
],
).opacity(0.75);
+ case 'calls.start':
+ return Row(
+ children: [
+ const Icon(Symbols.call, size: 20),
+ const Gap(4),
+ Text(
+ 'callMessageStarted'.tr(),
+ ),
+ ],
+ ).opacity(0.75);
+ case 'calls.end':
+ return Row(
+ children: [
+ const Icon(Symbols.call_end, size: 20),
+ const Gap(4),
+ Text(
+ 'callMessageEnded'.tr(args: [
+ _formatDuration(Duration(seconds: data.body['last'])),
+ ]),
+ ),
+ ],
+ ).opacity(0.75);
default:
return Row(
children: [
diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart
index aacce88..c8fc447 100644
--- a/lib/widgets/chat/chat_message_input.dart
+++ b/lib/widgets/chat/chat_message_input.dart
@@ -36,6 +36,7 @@ class ChatMessageInputState extends State {
}
void setEdit(SnChatMessage? value) {
+ _contentController.text = value?.body['text'] ?? '';
setState(() => _editingMessage = value);
}
diff --git a/lib/widgets/connection_indicator.dart b/lib/widgets/connection_indicator.dart
index f0289ee..25a3704 100644
--- a/lib/widgets/connection_indicator.dart
+++ b/lib/widgets/connection_indicator.dart
@@ -17,38 +17,45 @@ class ConnectionIndicator extends StatelessWidget {
builder: (context, _) {
final ua = context.read();
- return Container(
- padding: EdgeInsets.only(
- bottom: 8,
- top: MediaQuery.of(context).padding.top + 8,
- left: 24,
- right: 24,
- ),
- color: Theme.of(context).colorScheme.secondaryContainer,
- child: ua.isAuthorized
- ? Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- if (ws.isBusy)
- Text('serverConnecting').tr().textColor(
- Theme.of(context).colorScheme.onSecondaryContainer)
- else if (!ws.isConnected)
- Text('serverDisconnected').tr().textColor(
- Theme.of(context).colorScheme.onSecondaryContainer),
- ],
- )
- : const SizedBox.shrink(),
- )
- .height(
- (ws.isBusy || !ws.isConnected) && ua.isAuthorized
- ? MediaQuery.of(context).padding.top + 36
- : 0,
- animate: true)
- .animate(
- const Duration(milliseconds: 300),
- Curves.easeInOut,
- );
+ return GestureDetector(
+ child: Container(
+ padding: EdgeInsets.only(
+ bottom: 8,
+ top: MediaQuery.of(context).padding.top + 8,
+ left: 24,
+ right: 24,
+ ),
+ color: Theme.of(context).colorScheme.secondaryContainer,
+ child: ua.isAuthorized
+ ? Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ if (ws.isBusy)
+ Text('serverConnecting').tr().textColor(
+ Theme.of(context).colorScheme.onSecondaryContainer)
+ else if (!ws.isConnected)
+ Text('serverDisconnected').tr().textColor(
+ Theme.of(context).colorScheme.onSecondaryContainer),
+ ],
+ )
+ : const SizedBox.shrink(),
+ )
+ .height(
+ (ws.isBusy || !ws.isConnected) && ua.isAuthorized
+ ? MediaQuery.of(context).padding.top + 36
+ : 0,
+ animate: true)
+ .animate(
+ const Duration(milliseconds: 300),
+ Curves.easeInOut,
+ ),
+ onTap: () {
+ if (!ws.isConnected && !ws.isBusy) {
+ ws.connect();
+ }
+ },
+ );
},
);
}