Merge pull request #155 from tommyxchow/90-redesign

Refactor chat message widget and other chat fixes
This commit is contained in:
Tommy Chow 2022-07-05 04:38:38 -04:00 • committed by GitHub
commit c9cdd663bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 214 deletions

View file

@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
import 'package:frosty/screens/channel/stores/chat_store.dart';
import 'package:frosty/widgets/button.dart';
@ -37,55 +35,15 @@ class Chat extends StatelessWidget {
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
child: Observer(
builder: (context) {
final showDividers = chatStore.settings.showChatMessageDividers;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 5.0),
return ListView.builder(
padding: EdgeInsets.zero,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: chatStore.messages.length,
controller: chatStore.scrollController,
separatorBuilder: (context, index) => showDividers
? Divider(
height: chatStore.settings.messageSpacing,
thickness: 1.0,
)
: SizedBox(height: chatStore.settings.messageSpacing),
itemBuilder: (context, index) => Observer(
builder: (context) {
final message = chatStore.messages[index];
final chatMessage = ChatMessage(
ircMessage: message,
assetsStore: chatStore.assetsStore,
settingsStore: chatStore.settings,
);
if (message.user != null && message.user != chatStore.auth.user.details?.login) {
return InkWell(
onTap: () {
FocusScope.of(context).unfocus();
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
},
onLongPress: () {
HapticFeedback.lightImpact();
showModalBottomSheet(
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => ChatUserModal(
chatStore: chatStore,
username: message.user!,
userId: message.tags['user-id']!,
displayName: message.tags['display-name']!,
),
);
},
child: chatMessage,
);
}
return chatMessage;
},
itemBuilder: (context, index) => ChatMessage(
ircMessage: chatStore.messages[index],
chatStore: chatStore,
),
);
},

View file

@ -1,160 +1,209 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/models/irc.dart';
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
import 'package:frosty/screens/channel/stores/chat_store.dart';
import 'package:frosty/widgets/alert_message.dart';
class ChatMessage extends StatelessWidget {
final IRCMessage ircMessage;
final ChatAssetsStore assetsStore;
final SettingsStore settingsStore;
final ChatStore chatStore;
final bool isModal;
const ChatMessage({
Key? key,
required this.ircMessage,
required this.assetsStore,
required this.settingsStore,
required this.chatStore,
this.isModal = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const highlightPadding = EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0);
const messagePadding = EdgeInsets.symmetric(horizontal: 10.0);
return Observer(
builder: (context) {
Color? color;
final Widget renderMessage;
switch (ircMessage.command) {
case Command.privateMessage:
case Command.userState:
// If user is being mentioned in the message, highlight it red.
if (ircMessage.mention == true) color = Colors.red.withOpacity(0.3);
switch (ircMessage.command) {
case Command.privateMessage:
case Command.userState:
// Render normal chat message (PRIVMSG).
final child = Observer(
builder: (context) => Text.rich(
TextSpan(
children: ircMessage.generateSpan(
style: DefaultTextStyle.of(context).style,
assetsStore: assetsStore,
emoteScale: settingsStore.emoteScale,
badgeScale: settingsStore.badgeScale,
useZeroWidth: settingsStore.showZeroWidth,
useReadableColors: settingsStore.useReadableColors,
isLightTheme: Theme.of(context).brightness == Brightness.light,
launchExternal: settingsStore.launchUrlExternal,
timestamp: settingsStore.timestampType,
renderMessage = Text.rich(
TextSpan(
children: ircMessage.generateSpan(
style: DefaultTextStyle.of(context).style,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useZeroWidth: chatStore.settings.showZeroWidth,
useReadableColors: chatStore.settings.useReadableColors,
isLightTheme: Theme.of(context).brightness == Brightness.light,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
),
),
);
);
break;
case Command.clearChat:
case Command.clearMessage:
// Render timeouts and bans
final banDuration = ircMessage.tags['ban-duration'];
renderMessage = Opacity(
opacity: 0.5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
style: DefaultTextStyle.of(context).style,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
showMessage: chatStore.settings.showDeletedMessages,
useZeroWidth: chatStore.settings.showZeroWidth,
useReadableColors: chatStore.settings.useReadableColors,
isLightTheme: Theme.of(context).brightness == Brightness.light,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
),
const SizedBox(height: 5.0),
if (banDuration == null)
if (ircMessage.command == Command.clearMessage)
const Text(
'Message deleted',
style: TextStyle(fontWeight: FontWeight.w600),
)
else
const Text(
'User permanently banned',
style: TextStyle(fontWeight: FontWeight.w600),
)
else
Text(
'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}',
style: const TextStyle(fontWeight: FontWeight.w600),
)
],
),
);
break;
case Command.notice:
renderMessage = Text.rich(
TextSpan(text: ircMessage.message),
style: TextStyle(color: Theme.of(context).textTheme.bodyText2?.color?.withOpacity(0.5)),
);
break;
case Command.userNotice:
color = Colors.deepPurple.withOpacity(0.3);
if (ircMessage.mention == true) {
return Container(
padding: highlightPadding,
color: const Color(0x4DFF0000),
child: child,
);
}
return Padding(
padding: messagePadding,
child: child,
);
case Command.clearChat:
case Command.clearMessage:
// Render timeouts and bans
final banDuration = ircMessage.tags['ban-duration'];
return Padding(
padding: highlightPadding,
child: Opacity(
opacity: 0.50,
child: Column(
renderMessage = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Observer(
builder: (context) => Text.rich(
TextSpan(
children: ircMessage.generateSpan(
style: DefaultTextStyle.of(context).style,
assetsStore: assetsStore,
emoteScale: settingsStore.emoteScale,
badgeScale: settingsStore.badgeScale,
showMessage: settingsStore.showDeletedMessages,
useZeroWidth: settingsStore.showZeroWidth,
useReadableColors: settingsStore.useReadableColors,
isLightTheme: Theme.of(context).brightness == Brightness.light,
launchExternal: settingsStore.launchUrlExternal,
timestamp: settingsStore.timestampType,
),
),
),
),
const SizedBox(height: 5.0),
if (banDuration == null)
if (ircMessage.command == Command.clearMessage)
const Text(
'Message deleted',
style: TextStyle(fontWeight: FontWeight.bold),
)
else
const Text(
'User permanently banned',
style: TextStyle(fontWeight: FontWeight.bold),
)
else
if (ircMessage.tags.containsKey('system-msg'))
Text(
'Timed out for $banDuration ${int.parse(banDuration) > 1 ? 'seconds' : 'second'}',
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
);
case Command.notice:
return Padding(
padding: messagePadding,
child: Text.rich(
TextSpan(text: ircMessage.message),
style: TextStyle(color: Theme.of(context).textTheme.bodyText2?.color?.withOpacity(0.5)),
),
);
case Command.userNotice:
return Container(
padding: highlightPadding,
color: const Color(0x339146FF),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (ircMessage.tags.containsKey('system-msg')) Text(ircMessage.tags['system-msg']!),
if (ircMessage.tags.containsKey('msg-id') && ircMessage.tags['msg-id'] == 'announcement')
Row(
children: const [
Icon(Icons.announcement),
SizedBox(width: 5.0),
Text(
'Announcement',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 5.0),
if (ircMessage.message != null)
Observer(
builder: (context) => Text.rich(
ircMessage.tags['system-msg']!,
style: const TextStyle(fontWeight: FontWeight.w600),
),
if (ircMessage.tags.containsKey('msg-id') && ircMessage.tags['msg-id'] == 'announcement')
Row(
children: const [
Icon(Icons.announcement),
SizedBox(width: 5.0),
Text(
'Announcement',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 5.0),
if (ircMessage.message != null)
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
style: DefaultTextStyle.of(context).style,
assetsStore: assetsStore,
emoteScale: settingsStore.emoteScale,
badgeScale: settingsStore.badgeScale,
useZeroWidth: settingsStore.showZeroWidth,
useReadableColors: settingsStore.useReadableColors,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useZeroWidth: chatStore.settings.showZeroWidth,
useReadableColors: chatStore.settings.useReadableColors,
isLightTheme: Theme.of(context).brightness == Brightness.light,
launchExternal: settingsStore.launchUrlExternal,
timestamp: settingsStore.timestampType,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
),
),
),
],
),
],
);
break;
default:
renderMessage = const SizedBox();
}
final paddedMessage = Padding(
padding: EdgeInsets.symmetric(vertical: chatStore.settings.messageSpacing / 2, horizontal: 10.0),
child: renderMessage,
);
default:
return const SizedBox();
}
// Add a divider above the message if dividers are enabled.
final dividedMessage = chatStore.settings.showChatMessageDividers
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
paddedMessage,
const Divider(
height: 1.0,
thickness: 1.0,
),
],
)
: paddedMessage;
// Color the message if the color has been set.
final coloredMessage = color == null ? dividedMessage : ColoredBox(color: color, child: dividedMessage);
return InkWell(
onTap: () {
FocusScope.of(context).unfocus();
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
},
onLongPress: isModal
? () {
HapticFeedback.lightImpact();
Clipboard.setData(ClipboardData(text: ircMessage.message));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: AlertMessage(message: 'Message copied to clipboard'),
behavior: SnackBarBehavior.floating,
),
);
}
: ircMessage.user != null && ircMessage.user != chatStore.auth.user.details?.login
? () {
HapticFeedback.lightImpact();
showModalBottomSheet(
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => ChatUserModal(
chatStore: chatStore,
username: ircMessage.user!,
userId: ircMessage.tags['user-id']!,
displayName: ircMessage.tags['display-name']!,
),
);
}
: null,
child: coloredMessage,
);
},
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/constants/constants.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
import 'package:frosty/screens/channel/stores/chat_store.dart';
@ -31,7 +32,6 @@ class ChatUserModal extends StatefulWidget {
class _ChatUserModalState extends State<ChatUserModal> {
@override
Widget build(BuildContext context) {
final userMessages = widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList();
final name = regexEnglish.hasMatch(widget.displayName) ? widget.displayName : '${widget.displayName} (${widget.username})';
return FrostyModal(
@ -90,48 +90,33 @@ class _ChatUserModalState extends State<ChatUserModal> {
'Recent Messages',
padding: EdgeInsets.all(10.0),
),
if (userMessages.isEmpty)
const Expanded(child: AlertMessage(message: 'No recent messages'))
else
Expanded(
child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale),
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize),
child: ListView.separated(
padding: EdgeInsets.zero,
reverse: true,
itemBuilder: (context, index) => InkWell(
onLongPress: () async {
HapticFeedback.lightImpact();
Expanded(
child: Observer(
builder: (context) {
final userMessages = widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList();
await Clipboard.setData(ClipboardData(text: userMessages[index].message));
if (userMessages.isEmpty) {
return const AlertMessage(message: 'No recent messages');
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: AlertMessage(message: 'Message copied to clipboard'),
behavior: SnackBarBehavior.floating,
),
);
},
child: ChatMessage(
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale),
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize),
child: ListView.builder(
reverse: true,
itemCount: userMessages.length,
itemBuilder: (context, index) => ChatMessage(
ircMessage: userMessages[index],
assetsStore: widget.chatStore.assetsStore,
settingsStore: widget.chatStore.settings,
chatStore: widget.chatStore,
isModal: true,
),
),
separatorBuilder: (context, index) => widget.chatStore.settings.showChatMessageDividers
? Divider(
height: widget.chatStore.settings.messageSpacing,
thickness: 1.0,
)
: SizedBox(height: widget.chatStore.settings.messageSpacing),
itemCount: userMessages.length,
),
),
),
);
},
),
)
],
),
),

View file

@ -21,7 +21,7 @@ class _ChatSettingsState extends State<ChatSettings> {
@override
Widget build(BuildContext context) {
const timestamps = ['Disabled', '12-Hour', '24-Hour'];
const timestamps = ['Disabled', '12-hour', '24-hour'];
const sectionPadding = EdgeInsets.only(left: 16.0, bottom: 5.0, top: 30.0);
@ -96,6 +96,7 @@ class _ChatSettingsState extends State<ChatSettings> {
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Slider.adaptive(
value: settingsStore.chatWidth,