Compare commits

...

4 Commits

9 changed files with 128 additions and 44 deletions

View File

@ -483,6 +483,9 @@ class HttpFileTransferService {
mime: mime,
);
final newConv = conv.copyWith(
lastMessage: conv.lastMessage?.id == job.mId ?
msg :
conv.lastMessage,
sharedMedia: [
sharedMedium,
...conv.sharedMedia,

View File

@ -100,11 +100,11 @@ class NotificationsService {
title: c.title,
body: body,
largeIcon: c.avatarUrl.isNotEmpty ? 'file://${c.avatarUrl}' : null,
notificationLayout: m.thumbnailable ?
notificationLayout: m.isThumbnailable ?
NotificationLayout.BigPicture :
NotificationLayout.Messaging,
category: NotificationCategory.Message,
bigPicture: m.thumbnailable ? 'file://${m.mediaUrl}' : null,
bigPicture: m.isThumbnailable ? 'file://${m.mediaUrl}' : null,
payload: <String, String>{
'conversationJid': c.jid,
'sid': m.sid,

View File

@ -201,19 +201,46 @@ String? guessMimeTypeFromExtension(String ext) {
return null;
}
/// Return the translated name describing the MIME type [mime]. If [mime] is null or
/// the MIME type is neither image, video or audio, then it falls back to the
/// translation of "file".
String mimeTypeToName(String? mime) {
if (mime != null) {
if (mime.startsWith('image')) {
return t.messages.image;
} else if (mime.startsWith('audio')) {
return t.messages.audio;
} else if (mime.startsWith('video')) {
return t.messages.video;
}
}
return t.messages.file;
}
/// Return an emoji for the MIME type [mime]. If [addTypeName] id true, then a human readable
/// name for the MIME type will be appended.
String mimeTypeToEmoji(String? mime, {bool addTypeName = true}) {
String value;
if (mime != null) {
if (mime.startsWith('image')) {
return '🖼️${addTypeName ? " ${t.messages.image}" : ""}';
value = '🖼️';
} else if (mime.startsWith('audio')) {
return '🎙${addTypeName ? " ${t.messages.audio}" : ""}';
value = '🎙';
} else if (mime.startsWith('video')) {
return '🎬${addTypeName ? " ${t.messages.video}" : ""}';
value = '🎬';
} else {
value = '📁';
}
} else {
value = '📁';
}
return '📁${addTypeName ? " ${t.messages.file}" : ""}';
if (addTypeName) {
value += ' ${mimeTypeToName(mime)}';
}
return value;
}
/// Parse an Uri and return the "filename".

View File

@ -150,7 +150,7 @@ class Message with _$Message {
/// Returns true if the message contains media that can be thumbnailed, i.e. videos or
/// images.
bool get thumbnailable => isMedia && mediaType != null && (
bool get isThumbnailable => isMedia && mediaType != null && (
mediaType!.startsWith('image/') ||
mediaType!.startsWith('video/')
);

View File

@ -41,7 +41,7 @@ class SendFilesPage extends StatelessWidget {
padding: const EdgeInsets.only(right: 4),
child: SharedImageWidget(
path,
() {
onTap: () {
if (selected) {
// The trash can icon has been tapped
context.read<SendFilesBloc>().add(

View File

@ -182,9 +182,14 @@ Widget buildQuoteMessageWidget(Message message, bool sent, { void Function()? re
Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
if (medium.mime == null) {
return SharedFileWidget(medium.path);
return SharedFileWidget(
medium.path,
);
} else if (medium.mime!.startsWith('image/')) {
return SharedImageWidget(medium.path, () => OpenFile.open(medium.path));
return SharedImageWidget(
medium.path,
onTap: () => OpenFile.open(medium.path),
);
} else if (medium.mime!.startsWith('video/')) {
return SharedVideoWidget(
medium.path,

View File

@ -23,22 +23,34 @@ const sharedMediaContainerDimension = 75.0;
/// A widget to show a message that was sent within a chat or is about to be sent.
class SharedMediaContainer extends StatelessWidget {
const SharedMediaContainer(this.child, { this.onTap, super.key });
const SharedMediaContainer(this.child, {
this.onTap,
this.size = sharedMediaContainerDimension,
super.key,
}
);
final Widget? child;
final void Function()? onTap;
final double size;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: SizedBox(
height: sharedMediaContainerDimension,
width: sharedMediaContainerDimension,
child: AspectRatio(
aspectRatio: 1,
child: child,
),
final childWidget = SizedBox(
height: size,
width: size,
child: AspectRatio(
aspectRatio: 1,
child: child,
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
child: childWidget,
);
}
return childWidget;
}
}

View File

@ -3,18 +3,29 @@ import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
class SharedImageWidget extends StatelessWidget {
const SharedImageWidget(this.path, this.onTap, { this.borderColor, this.child, super.key });
const SharedImageWidget(
this.path, {
this.onTap,
this.borderColor,
this.child,
this.borderRadius = 10,
this.size = sharedMediaContainerDimension,
super.key,
}
);
final String path;
final Color? borderColor;
final void Function() onTap;
final void Function()? onTap;
final Widget? child;
final double borderRadius;
final double size;
@override
Widget build(BuildContext context) {
return SharedMediaContainer(
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(borderRadius),
border: borderColor != null ? Border.all(
color: borderColor!,
width: 4,
@ -27,6 +38,7 @@ class SharedImageWidget extends StatelessWidget {
clipBehavior: Clip.hardEdge,
child: child,
),
size: size,
onTap: onTap,
);
}

View File

@ -10,6 +10,7 @@ import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/widgets/avatar.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
import 'package:moxxyv2/ui/widgets/chat/typing.dart';
class ConversationsListRow extends StatefulWidget {
@ -85,12 +86,21 @@ class ConversationsListRowState extends State<ConversationsListRow> {
return const TypingIndicatorWidget(Colors.black, Colors.white);
}
final lastMessage = widget.conversation.lastMessage;
String body;
if (widget.conversation.lastMessage == null) {
if (lastMessage == null) {
body = '';
} else {
if (widget.conversation.lastMessage!.isRetracted) {
if (lastMessage.isRetracted) {
body = t.messages.retracted;
} else if (lastMessage.isMedia) {
// If the file is thumbnailable, we display a small preview on the left of the
// body, so we don't need the emoji then.
if (lastMessage.isThumbnailable) {
body = mimeTypeToName(lastMessage.mediaType);
} else {
body = mimeTypeToEmoji(lastMessage.mediaType);
}
} else {
body = widget.conversation.lastMessage!.body;
}
@ -183,26 +193,41 @@ class ConversationsListRowState extends State<ConversationsListRow> {
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
LimitedBox(
maxWidth: textWidth,
child: _buildLastMessageBody(),
),
const Spacer(),
Visibility(
visible: showBadge,
child: Badge(
badgeContent: Text(badgeText),
badgeColor: bubbleColorSent,
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
...widget.conversation.lastMessage?.isThumbnailable == true ? [
Padding(
padding: const EdgeInsets.only(right: 5),
child: SharedImageWidget(
widget.conversation.lastMessage!.mediaUrl!,
borderRadius: 5,
size: 30,
),
),
] : [
const SizedBox(height: 30),
],
LimitedBox(
maxWidth: textWidth,
child: _buildLastMessageBody(),
),
),
Visibility(
visible: sentBySelf,
child: _getLastMessageIcon(),
),
],
const Spacer(),
Visibility(
visible: showBadge,
child: Badge(
badgeContent: Text(badgeText),
badgeColor: bubbleColorSent,
),
),
Visibility(
visible: sentBySelf,
child: _getLastMessageIcon(),
),
],
),
),
],
),