💄 Optimization of UX in messages

This commit is contained in:
LittleSheep 2024-11-25 00:05:49 +08:00
parent 9b6544df46
commit a59de65130
12 changed files with 170 additions and 94 deletions

View File

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

View File

@ -228,5 +228,7 @@
"callVideoFlip": "镜像画面",
"callSpeakerphoneToggle": "切换扬声器",
"callScreenOff": "关闭屏幕共享",
"callScreenOn": "开启屏幕共享"
"callScreenOn": "开启屏幕共享",
"callMessageEnded": "通话持续了 {}",
"callMessageStarted": "通话开始了"
}

View File

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

View File

@ -45,6 +45,8 @@
<array>
<string>fetch</string>
<string>remote-notification</string>
<string>audio</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

View File

@ -4,5 +4,7 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict>
</plist>

View File

@ -35,7 +35,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
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<CallRoomScreen> {
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 !=

View File

@ -138,6 +138,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
);
}
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<ChatRoomScreen> {
},
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,

View File

@ -86,22 +86,28 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
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: [

View File

@ -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<PostEditorScreen> {
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),

View File

@ -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: [

View File

@ -36,6 +36,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}
void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? '';
setState(() => _editingMessage = value);
}

View File

@ -17,38 +17,45 @@ class ConnectionIndicator extends StatelessWidget {
builder: (context, _) {
final ua = context.read<UserProvider>();
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();
}
},
);
},
);
}