Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
Tommy Chow
3069cd93ab Merge branch 'main' into chat-improvements 2024-09-01 15:37:54 -07:00
Tommy Chow
f9b7e2a4bd Style reply username in reply header 2023-12-20 23:27:37 -05:00
Tommy Chow
0d8b42b100 Only markup mention if callback provided 2023-12-20 22:56:17 -05:00
Tommy Chow
bc0bb11f14 Merge branch 'main' into chat-improvements 2023-12-20 22:33:43 -05:00
Tommy Chow
0f9fbe0703 Use null for proper ink effect 2023-12-18 02:42:01 -05:00
Tommy Chow
f89e43a93e Decrease chat name font weight 2023-12-18 02:32:26 -05:00
Tommy Chow
1125c06823 Improve affordance for user mentions in chat and fix emote/badge ink effects 2023-12-18 02:17:02 -05:00
2 changed files with 148 additions and 78 deletions

View file

@ -112,7 +112,6 @@ class IRCMessage {
/// Returns an [InlineSpan] list that corresponds to the badges, username, words, and emotes of the given [IRCMessage].
List<InlineSpan> generateSpan(
BuildContext context, {
TextStyle? style,
required ChatAssetsStore assetsStore,
required double badgeScale,
required double emoteScale,
@ -123,6 +122,7 @@ class IRCMessage {
bool useReadableColors = false,
TimestampType timestamp = TimestampType.disabled,
}) {
final style = DefaultTextStyle.of(context).style;
final isLightTheme = Theme.of(context).brightness == Brightness.light;
final emoteToObject = assetsStore.emoteToObject;
@ -146,7 +146,7 @@ class IRCMessage {
span.add(
TextSpan(
text: '${DateFormat.Hm().format(parsedTime)} ',
style: style?.copyWith(color: style.color?.withOpacity(0.5)),
style: style.copyWith(color: style.color?.withOpacity(0.5)),
),
);
}
@ -155,7 +155,7 @@ class IRCMessage {
span.add(
TextSpan(
text: '${DateFormat('h:mm').format(parsedTime)} ',
style: style?.copyWith(color: style.color?.withOpacity(0.5)),
style: style.copyWith(color: style.color?.withOpacity(0.5)),
),
);
}
@ -328,7 +328,7 @@ class IRCMessage {
text: user != null ? getReadableName(displayName, user!) : displayName,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
),
recognizer: TapGestureRecognizer()..onTap = onTapName,
),
@ -339,7 +339,7 @@ class IRCMessage {
// Italicize the text if it was called with an IRC Action (e.g., "/me").
final textStyle =
action == true ? const TextStyle(fontStyle: FontStyle.italic) : style;
style.copyWith(fontStyle: action == true ? FontStyle.italic : null);
if (!showMessage) {
span.add(const TextSpan(text: ' <message deleted>'));
@ -390,7 +390,7 @@ class IRCMessage {
if (nextWordIsEmoji)
Text(
words[index],
style: textStyle?.copyWith(fontSize: emoteSize - 5),
style: textStyle.copyWith(fontSize: emoteSize - 5),
),
...emoteStack.reversed.map(
(emote) => FrostyCachedNetworkImage(
@ -423,7 +423,7 @@ class IRCMessage {
if (nextWordIsEmoji)
Text(
emoji,
style: textStyle?.copyWith(fontSize: 40),
style: textStyle.copyWith(fontSize: 40),
),
...emoteStack.reversed.map(
(emote) => FrostyCachedNetworkImage(
@ -455,6 +455,7 @@ class IRCMessage {
localSpan.add(const TextSpan(text: ' '));
localSpan.add(
_createTextSpan(
context,
text: words[index],
style: textStyle,
launchExternal: launchExternal,
@ -478,14 +479,18 @@ class IRCMessage {
} else {
if (regexEmoji.hasMatch(word)) {
localSpan.add(
_createEmojiSpan(
emoji: word,
style: textStyle?.copyWith(fontSize: emoteSize - 5),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Text(
word,
style: textStyle.copyWith(fontSize: emoteSize - 5),
),
),
);
} else {
localSpan.add(
_createTextSpan(
context,
text: word,
style: textStyle,
launchExternal: launchExternal,
@ -507,19 +512,6 @@ class IRCMessage {
return span;
}
static WidgetSpan _createEmojiSpan({
required String emoji,
TextStyle? style,
}) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Text(
emoji,
style: style,
),
);
}
static Widget _createBadgeWidget({
required ChatBadge badge,
double? size,
@ -562,27 +554,36 @@ class IRCMessage {
}) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: InkWell(
onTap: () => _showAssetDetailsBottomSheet(
context,
leading: _createBadgeWidget(
child: Stack(
children: [
_createBadgeWidget(
badge: badge,
size: size,
backgroundColor: backgroundColor,
isSvg: isSvg,
size: 56,
),
url: badge.url,
title: badge.name,
subtitle: Text(badge.type.toString()),
launchExternal: launchExternal,
showCopyName: false,
),
child: _createBadgeWidget(
badge: badge,
size: size,
backgroundColor: backgroundColor,
isSvg: isSvg,
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showAssetDetailsBottomSheet(
context,
leading: _createBadgeWidget(
badge: badge,
backgroundColor: backgroundColor,
isSvg: isSvg,
size: 56,
),
url: badge.url,
title: badge.name,
subtitle: Text(badge.type.toString()),
launchExternal: launchExternal,
showCopyName: false,
),
),
),
),
],
),
);
}
@ -596,44 +597,76 @@ class IRCMessage {
}) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: InkWell(
onTap: () => showEmoteDetailsBottomSheet(
context,
emote: emote,
launchExternal: launchExternal,
),
child: FrostyCachedNetworkImage(
imageUrl: emote.url,
height: height,
width: width,
useFade: false,
placeholder: (context, url) => const SizedBox(),
),
child: Stack(
children: [
FrostyCachedNetworkImage(
imageUrl: emote.url,
height: height,
width: width,
useFade: false,
placeholder: (context, url) => const SizedBox(),
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => showEmoteDetailsBottomSheet(
context,
emote: emote,
launchExternal: launchExternal,
),
),
),
),
],
),
);
}
static TextSpan _createTextSpan({
static InlineSpan _createTextSpan(
BuildContext context, {
required String text,
required bool launchExternal,
TextStyle? style,
required TextStyle style,
Function(String)? onTapPingedUser,
}) {
if (text.startsWith('@')) {
return TextSpan(
text: text,
style: style?.copyWith(fontWeight: FontWeight.bold),
recognizer: TapGestureRecognizer()
..onTap = () {
if (onTapPingedUser != null) {
onTapPingedUser(text.substring(1).split(',')[0]);
}
},
final isLight = Theme.of(context).brightness == Brightness.light;
final mention = text.split(',')[0];
final mentionTextWidget = Text(
mention,
style: style.copyWith(
fontWeight: FontWeight.w600,
),
);
const borderRadius = BorderRadius.all(Radius.circular(4));
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: onTapPingedUser != null
? Material(
borderRadius: borderRadius,
child: InkWell(
borderRadius: borderRadius,
onTap: () => onTapPingedUser(mention.substring(1)),
child: Ink(
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color:
isLight ? Colors.grey.shade200 : Colors.grey.shade900,
),
child: mentionTextWidget,
),
),
)
: mentionTextWidget,
);
} else if (RegExp(r'https?:\/\/').hasMatch(text)) {
return TextSpan(
text: text,
style: style?.copyWith(
style: style.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
decorationColor: Colors.blue,

View file

@ -47,9 +47,6 @@ class ChatMessage extends StatelessWidget {
BuildContext context, {
required String nickname,
}) {
// Ignore if the message is a recent message in the modal bottom sheet.
if (isModal) return;
final twitchApi = context.read<TwitchApi>();
final authStore = context.read<AuthStore>();
twitchApi
@ -79,7 +76,7 @@ class ChatMessage extends StatelessWidget {
chatStore.updateNotification('Message copied');
}
void onLongPressMessage(BuildContext context, TextStyle defaultTextStyle) {
void onLongPressMessage(BuildContext context) {
HapticFeedback.lightImpact();
if (ircMessage.command != Command.privateMessage &&
@ -107,7 +104,7 @@ class ChatMessage extends StatelessWidget {
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),
style: defaultTextStyle,
style: DefaultTextStyle.of(context).style,
),
),
),
@ -160,9 +157,10 @@ class ChatMessage extends StatelessWidget {
children: ircMessage.generateSpan(
context,
onTapName: () => onTapName(context),
onTapPingedUser: (nickname) =>
onTapPingedUser(context, nickname: nickname),
style: defaultTextStyle,
onTapPingedUser: isModal
? null
: (nickname) =>
onTapPingedUser(context, nickname: nickname),
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
@ -179,6 +177,7 @@ class ChatMessage extends StatelessWidget {
Widget? messageHeaderIcon;
Widget? messageHeader;
const mentionBorderRadius = BorderRadius.all(Radius.circular(4));
if (replyUser != null && replyBody != null) {
messageHeaderIcon = Icon(
Icons.chat_rounded,
@ -197,8 +196,48 @@ class ChatMessage extends StatelessWidget {
);
},
),
child: Text(
'Replying to @$replyUser: $replyBody',
child: Text.rich(
TextSpan(
children: [
const TextSpan(
text: 'Replying to ',
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Material(
borderRadius: mentionBorderRadius,
child: InkWell(
borderRadius: mentionBorderRadius,
onTap: () =>
onTapPingedUser(context, nickname: replyUser),
child: Ink(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).brightness ==
Brightness.light
? Colors.grey.shade200
: Colors.grey.shade900,
),
child: Text(
'@$replyUser',
style: defaultTextStyle.copyWith(
fontWeight: FontWeight.w600,
color: messageHeaderTextColor,
),
),
),
),
),
),
TextSpan(
text: ': $replyBody',
),
],
),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
@ -292,7 +331,6 @@ class ChatMessage extends StatelessWidget {
children: ircMessage.generateSpan(
context,
onTapName: () => onTapName(context),
style: defaultTextStyle,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
@ -385,7 +423,6 @@ class ChatMessage extends StatelessWidget {
children: ircMessage.generateSpan(
context,
onTapName: () => onTapName(context),
style: defaultTextStyle,
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
@ -447,7 +484,7 @@ class ChatMessage extends StatelessWidget {
chatStore.assetsStore.showEmoteMenu = false;
}
},
onLongPress: () => onLongPressMessage(context, defaultTextStyle),
onLongPress: () => onLongPressMessage(context),
child: coloredMessage,
);