Compare commits

...

10 Commits

29 changed files with 379 additions and 275 deletions

View File

@ -73,7 +73,9 @@
"fileNotEncrypted": "The chat is encrypted but the file is not encrypted" "fileNotEncrypted": "The chat is encrypted but the file is not encrypted"
}, },
"conversation": { "conversation": {
"audioRecordingError": "Failed to finalize audio recording" "audioRecordingError": "Failed to finalize audio recording",
"openFileNoAppError": "No app found to open this file",
"openFileGenericError": "Failed to open file"
} }
}, },
"warnings": { "warnings": {
@ -127,7 +129,10 @@
"showWarning": "Show warning", "showWarning": "Show warning",
"addToContacts": "Add to contacts", "addToContacts": "Add to contacts",
"addToContactsTitle": "Add ${jid} to contacts", "addToContactsTitle": "Add ${jid} to contacts",
"addToContactsBody": "Are you sure you want to add ${jid} to your contacts?" "addToContactsBody": "Are you sure you want to add ${jid} to your contacts?",
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
"stickerSettings": "Sticker settings"
}, },
"addcontact": { "addcontact": {
"title": "Add new contact", "title": "Add new contact",

View File

@ -73,7 +73,9 @@
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen" "fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
}, },
"conversation": { "conversation": {
"audioRecordingError": "Fehler beim Fertigstellen der Audioaufnahme" "audioRecordingError": "Fehler beim Fertigstellen der Audioaufnahme",
"openFileNoAppError": "Keine App vorhanden, um die Datei zu öffnen",
"openFileGenericError": "Fehler beim Öffnen der Datei"
} }
}, },
"warnings": { "warnings": {
@ -127,7 +129,10 @@
"showWarning": "Warnung anzeigen", "showWarning": "Warnung anzeigen",
"addToContacts": "Zu Kontaken hinzufügen", "addToContacts": "Zu Kontaken hinzufügen",
"addToContactsTitle": "${jid} zu Kontakten hinzufügen", "addToContactsTitle": "${jid} zu Kontakten hinzufügen",
"addToContactsBody": "Bist du dir sicher, dass du ${jid} zu deinen Kontakten hinzufügen möchtest?" "addToContactsBody": "Bist du dir sicher, dass du ${jid} zu deinen Kontakten hinzufügen möchtest?",
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
"stickerSettings": "Stickereinstellungen"
}, },
"addcontact": { "addcontact": {
"title": "Neuen Kontakt hinzufügen", "title": "Neuen Kontakt hinzufügen",

View File

@ -276,6 +276,8 @@ class OmemoService {
final keys = List<OmemoDevice>.empty(growable: true); final keys = List<OmemoDevice>.empty(growable: true);
final tm = omemoState.trustManager as BlindTrustBeforeVerificationTrustManager; final tm = omemoState.trustManager as BlindTrustBeforeVerificationTrustManager;
final trustMap = await tm.getDevicesTrust(jid); final trustMap = await tm.getDevicesTrust(jid);
if (!_fingerprintCache.containsKey(jid)) return [];
for (final deviceId in _fingerprintCache[jid]!.keys) { for (final deviceId in _fingerprintCache[jid]!.keys) {
keys.add( keys.add(
OmemoDevice( OmemoDevice(
@ -341,22 +343,27 @@ class OmemoService {
); );
final bareJid = ownJid.toBare().toString(); final bareJid = ownJid.toBare().toString();
// Get finger prints if we have to // Get fingerprints if we have to
await _loadOrFetchFingerprints(ownJid); await _loadOrFetchFingerprints(ownJid);
_fingerprintCache[bareJid]!.forEach((deviceId, fingerprint) { final tm = omemoState.trustManager as BlindTrustBeforeVerificationTrustManager;
if (deviceId == ownId) return; final trustMap = await tm.getDevicesTrust(bareJid);
for (final deviceId in _fingerprintCache[bareJid]!.keys) {
if (deviceId == ownId) continue;
final fingerprint = _fingerprintCache[bareJid]![deviceId]!;
keys.add( keys.add(
OmemoDevice( OmemoDevice(
fingerprint, fingerprint,
false, await tm.isTrusted(bareJid, deviceId),
false, trustMap[deviceId] == BTBVTrustState.verified,
false, await tm.isEnabled(bareJid, deviceId),
deviceId, deviceId,
hasSessionWith: false, hasSessionWith: false,
), ),
); );
}); }
return keys; return keys;
} }

View File

@ -37,6 +37,11 @@ const double fontsizeBody = 15;
const double fontsizeBodyOnlyEmojis = 30; const double fontsizeBodyOnlyEmojis = 30;
const double fontsizeSubbody = 10; const double fontsizeSubbody = 10;
// The color for a shared media item
final Color sharedMediaItemBackgroundColor = Colors.grey.shade500;
// The color for a shared media summary
final Color sharedMediaSummaryBackgroundColor = Colors.grey.shade500;
// The translucent black we use when we need to ensure good contrast, for example when // The translucent black we use when we need to ensure good contrast, for example when
// displaying the download progress indicator. // displaying the download progress indicator.
final backdropBlack = Colors.black.withAlpha(150); final backdropBlack = Colors.black.withAlpha(150);

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:better_open_file/better_open_file.dart';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -353,3 +354,24 @@ Future<void> handleUri(String uriString) async {
mode: LaunchMode.externalNonBrowserApplication, mode: LaunchMode.externalNonBrowserApplication,
); );
} }
/// Open the file [path] using the system native means. Shows a toast if the
/// file cannot be opened.
Future<void> openFile(String path) async {
final result = await OpenFile.open(path);
if (result.type != ResultType.done) {
String message;
if (result.type == ResultType.noAppToOpen) {
message = t.errors.conversation.openFileNoAppError;
} else {
message = t.errors.conversation.openFileGenericError;
}
await Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.SNACKBAR,
);
}
}

View File

@ -502,27 +502,33 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
), ),
), ),
Positioned( BlocBuilder<ConversationBloc, ConversationState>(
right: 8, buildWhen: (prev, next) => prev.emojiPickerVisible != next.emojiPickerVisible ||
bottom: 80, prev.stickerPickerVisible != next.stickerPickerVisible,
child: Material( builder: (context, state) => Positioned(
color: const Color.fromRGBO(0, 0, 0, 0), right: 8,
child: ScaleTransition( bottom: state.emojiPickerVisible || state.stickerPickerVisible ?
scale: _scrollToBottom, 330 /* 80 + 250 */ :
alignment: FractionalOffset.center, 80,
child: SizedBox( child: Material(
width: 45, color: const Color.fromRGBO(0, 0, 0, 0),
height: 45, child: ScaleTransition(
child: FloatingActionButton( scale: _scrollToBottom,
heroTag: 'fabScrollDown', alignment: FractionalOffset.center,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, child: SizedBox(
onPressed: () { width: 45,
_scrollController.jumpTo(0); height: 45,
}, child: FloatingActionButton(
child: const Icon( heroTag: 'fabScrollDown',
Icons.arrow_downward, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
// TODO(Unknown): Theme dependent onPressed: () {
color: Colors.white, _scrollController.jumpTo(0);
},
child: const Icon(
Icons.arrow_downward,
// TODO(Unknown): Theme dependent
color: Colors.white,
),
), ),
), ),
), ),

View File

@ -95,18 +95,13 @@ class ConversationProfileHeader extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SharedMediaContainer( SharedMediaContainer(
ClipRRect( Icon(
borderRadius: BorderRadius.circular(10), conversation.muted ?
child: ColoredBox( Icons.do_not_disturb_on :
color: getTileColor(context), Icons.do_not_disturb_off,
child: Icon( size: 32,
conversation.muted ?
Icons.do_not_disturb_on :
Icons.do_not_disturb_off,
size: 32,
),
),
), ),
color: getTileColor(context),
onTap: () { onTap: () {
GetIt.I.get<ProfileBloc>().add( GetIt.I.get<ProfileBloc>().add(
MuteStateSetEvent( MuteStateSetEvent(
@ -134,16 +129,11 @@ class ConversationProfileHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
SharedMediaContainer( SharedMediaContainer(
ClipRRect( const Icon(
borderRadius: BorderRadius.circular(10), Icons.security_outlined,
child: ColoredBox( size: 32,
color: getTileColor(context),
child: const Icon(
Icons.security_outlined,
size: 32,
),
),
), ),
color: getTileColor(context),
onTap: () { onTap: () {
GetIt.I.get<DevicesBloc>().add(DevicesRequestedEvent(conversation.jid)); GetIt.I.get<DevicesBloc>().add(DevicesRequestedEvent(conversation.jid));
}, },

View File

@ -90,16 +90,11 @@ class SelfProfileHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
SharedMediaContainer( SharedMediaContainer(
ClipRRect( const Icon(
borderRadius: BorderRadius.circular(10), Icons.security_outlined,
child: ColoredBox( size: 32,
color: getTileColor(context),
child: const Icon(
Icons.security_outlined,
size: 32,
),
),
), ),
color: getTileColor(context),
onTap: () { onTap: () {
GetIt.I.get<OwnDevicesBloc>().add(OwnDevicesRequestedEvent()); GetIt.I.get<OwnDevicesBloc>().add(OwnDevicesRequestedEvent());
}, },

View File

@ -1,6 +1,16 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/constants.dart';
String _formatHalfFingerprint(String half) {
final p1 = half.substring(0, 8);
final p2 = half.substring(8, 16);
final p3 = half.substring(16, 24);
final p4 = half.substring(24, 32);
return '$p1 $p2 $p3 $p4';
}
class FingerprintListItem extends StatelessWidget { class FingerprintListItem extends StatelessWidget {
const FingerprintListItem( const FingerprintListItem(
this.fingerprint, this.fingerprint,
@ -26,14 +36,8 @@ class FingerprintListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final parts = List<String>.empty(growable: true);
for (var i = 0; i < 8; i++) {
final part = fingerprint.substring(i*8, (i+1)*8);
parts.add(part);
}
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
final fontSize = width * 0.04; final fontSize = width * 0.1;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card( child: Card(
@ -47,17 +51,25 @@ class FingerprintListItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( AutoSizeText(
spacing: 6, _formatHalfFingerprint(fingerprint.substring(0, 32)),
children: parts style: TextStyle(
.map((part_) => Text( fontFamily: 'RobotoMono',
part_, fontSize: fontSize,
style: TextStyle( ),
fontFamily: 'RobotoMono', textAlign: TextAlign.center,
fontSize: fontSize, maxLines: 1,
),
),).toList(),
), ),
AutoSizeText(
_formatHalfFingerprint(fingerprint.substring(32)),
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: fontSize,
),
textAlign: TextAlign.center,
maxLines: 1,
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@ -101,6 +101,7 @@ class SendFilesPage extends StatelessWidget {
? const Icon(Icons.delete, size: 32) ? const Icon(Icons.delete, size: 32)
: const Icon(Icons.file_present), : const Icon(Icons.file_present),
), ),
color: sharedMediaItemBackgroundColor,
onTap: () { onTap: () {
if (selected) { if (selected) {
// The trash can icon has been tapped // The trash can icon has been tapped
@ -199,14 +200,11 @@ class SendFilesPage extends StatelessWidget {
return _renderPreview(context, item, index == state.index, index); return _renderPreview(context, item, index == state.index, index);
} else { } else {
return SharedMediaContainer( return SharedMediaContainer(
DecoratedBox( const Icon(Icons.attach_file),
decoration: BoxDecoration( color: sharedMediaItemBackgroundColor,
borderRadius: BorderRadius.circular(10), onTap: () => context.read<SendFilesBloc>().add(
color: Colors.grey, AddFilesRequestedEvent(),
),
child: const Icon(Icons.attach_file),
), ),
onTap: () => context.read<SendFilesBloc>().add(AddFilesRequestedEvent()),
); );
} }
}, },

View File

@ -124,13 +124,8 @@ class StickerPackPage extends StatelessWidget {
} }
return SharedMediaContainer( return SharedMediaContainer(
ClipRRect( child,
borderRadius: BorderRadius.circular(10), color: color,
child: ColoredBox(
color: color,
child: child,
),
),
onTap: () { onTap: () {
if (state.stickerPack!.local) { if (state.stickerPack!.local) {
_onDeletePressed(context, state); _onDeletePressed(context, state);

View File

@ -57,8 +57,8 @@ class RawChatBubble extends StatelessWidget {
return message.stickerPackId != null && message.stickerHashKey != null; return message.stickerPackId != null && message.stickerHashKey != null;
} }
Color? _getBubbleColor(BuildContext context) { Color _getBubbleColor(BuildContext context) {
if (_shouldNotColorBubble()) return null; if (_shouldNotColorBubble()) return Colors.transparent;
// Color the bubble red if it should be encrypted but is not. // Color the bubble red if it should be encrypted but is not.
if (chatEncrypted && !message.encrypted) { if (chatEncrypted && !message.encrypted) {
@ -83,24 +83,24 @@ class RawChatBubble extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final borderRadius = getBorderRadius(sentBySelf, start, between, end); final borderRadius = getBorderRadius(sentBySelf, start, between, end);
return Container( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: maxWidth, maxWidth: maxWidth,
), ),
decoration: BoxDecoration( child: Material(
color: _getBubbleColor(context), color: _getBubbleColor(context),
borderRadius: borderRadius, borderRadius: borderRadius,
), child: Padding(
child: Padding( // NOTE: Images don't work well with padding here
// NOTE: Images don't work well with padding here padding: message.isMedia || message.quotes != null ?
padding: message.isMedia || message.quotes != null ? EdgeInsets.zero :
EdgeInsets.zero : const EdgeInsets.all(8),
const EdgeInsets.all(8), child: buildMessageWidget(
child: buildMessageWidget( message,
message, maxWidth,
maxWidth, borderRadius,
borderRadius, sentBySelf,
sentBySelf, ),
), ),
), ),
); );

View File

@ -262,6 +262,7 @@ class AudioChatState extends State<AudioChatWidget> {
(widget.message.filename ?? '') : (widget.message.filename ?? '') :
filenameFromUrl(widget.message.srcUrl!), filenameFromUrl(widget.message.srcUrl!),
widget.radius, widget.radius,
widget.maxWidth,
widget.sent, widget.sent,
extra: DownloadButton( extra: DownloadButton(
onPressed: () { onPressed: () {

View File

@ -33,8 +33,13 @@ class MediaBaseChatWidget extends StatelessWidget {
borderRadius: radius, borderRadius: radius,
child: background, child: background,
), ),
...gradient ? [BottomGradient(radius)] : [],
...extra != null ? [ extra! ] : [], if (gradient)
BottomGradient(radius),
if (extra != null)
extra!,
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -47,17 +52,13 @@ class MediaBaseChatWidget extends StatelessWidget {
], ],
); );
if (onTap != null) { return IntrinsicWidth(
return IntrinsicWidth( child: onTap != null ?
child: InkResponse( InkWell(
onTap: onTap, onTap: onTap,
child: content, child: content,
), ) :
); content,
} else { );
return IntrinsicWidth(
child: content,
);
}
} }
} }

View File

@ -1,11 +1,11 @@
import 'dart:core'; import 'dart:core';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:better_open_file/better_open_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/bottom.dart'; import 'package:moxxyv2/ui/widgets/chat/bottom.dart';
import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart'; import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart';
import 'package:moxxyv2/ui/widgets/chat/media/base.dart'; import 'package:moxxyv2/ui/widgets/chat/media/base.dart';
@ -18,6 +18,7 @@ class FileChatBaseWidget extends StatelessWidget {
this.icon, this.icon,
this.filename, this.filename,
this.radius, this.radius,
this.maxWidth,
this.sent, this.sent,
{ {
this.extra, this.extra,
@ -29,35 +30,43 @@ class FileChatBaseWidget extends StatelessWidget {
final IconData icon; final IconData icon;
final String filename; final String filename;
final BorderRadius radius; final BorderRadius radius;
final double maxWidth;
final Widget? extra; final Widget? extra;
final bool sent; final bool sent;
final void Function()? onTap; final void Function()? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaBaseChatWidget( return SizedBox(
Padding( width: maxWidth,
padding: const EdgeInsets.all(16), child: MediaBaseChatWidget(
child: Column( Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.all(16),
children: [ child: Row(
Icon( mainAxisSize: MainAxisSize.min,
icon, children: [
size: 128, Icon(
), icon,
size: 48,
),
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(left: 6),
child: Text(filename), child: AutoSizeText(
), filename,
], maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
), ),
MessageBubbleBottom(message, sent),
radius,
gradient: false,
extra: extra,
onTap: onTap,
), ),
MessageBubbleBottom(message, sent),
radius,
gradient: false,
extra: extra,
onTap: onTap,
); );
} }
} }
@ -68,6 +77,7 @@ class FileChatWidget extends StatelessWidget {
const FileChatWidget( const FileChatWidget(
this.message, this.message,
this.radius, this.radius,
this.maxWidth,
this.sent, this.sent,
{ {
this.extra, this.extra,
@ -77,6 +87,7 @@ class FileChatWidget extends StatelessWidget {
final Message message; final Message message;
final BorderRadius radius; final BorderRadius radius;
final bool sent; final bool sent;
final double maxWidth;
final Widget? extra; final Widget? extra;
Widget _buildNonDownloaded() { Widget _buildNonDownloaded() {
@ -85,6 +96,7 @@ class FileChatWidget extends StatelessWidget {
Icons.file_present, Icons.file_present,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: DownloadButton( extra: DownloadButton(
onPressed: () { onPressed: () {
@ -103,6 +115,7 @@ class FileChatWidget extends StatelessWidget {
Icons.file_present, Icons.file_present,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: ProgressWidget(id: message.id), extra: ProgressWidget(id: message.id),
); );
@ -114,27 +127,19 @@ class FileChatWidget extends StatelessWidget {
Icons.file_present, Icons.file_present,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
onTap: () { onTap: () {
OpenFile.open(message.mediaUrl); openFile(message.mediaUrl!);
}, },
); );
} }
Widget _buildWrapper() { @override
Widget build(BuildContext context) {
if (!message.isDownloading && message.mediaUrl != null) return _buildInner(); if (!message.isDownloading && message.mediaUrl != null) return _buildInner();
if (message.isFileUploadNotification || message.isDownloading) return _buildDownloading(); if (message.isFileUploadNotification || message.isDownloading) return _buildDownloading();
return _buildNonDownloaded(); return _buildNonDownloaded();
} }
@override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: Padding(
padding: const EdgeInsets.all(8),
child: _buildWrapper(),
),
);
}
} }

View File

@ -1,11 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:better_open_file/better_open_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/bottom.dart'; import 'package:moxxyv2/ui/widgets/chat/bottom.dart';
import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart'; import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart';
import 'package:moxxyv2/ui/widgets/chat/helpers.dart'; import 'package:moxxyv2/ui/widgets/chat/helpers.dart';
@ -61,6 +61,7 @@ class ImageChatWidget extends StatelessWidget {
Icons.image, Icons.image,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: ProgressWidget(id: message.id), extra: ProgressWidget(id: message.id),
); );
@ -86,9 +87,7 @@ class ImageChatWidget extends StatelessWidget {
image, image,
MessageBubbleBottom(message, sent), MessageBubbleBottom(message, sent),
radius, radius,
onTap: () { onTap: () => openFile(message.mediaUrl!),
OpenFile.open(message.mediaUrl);
},
); );
} }
@ -118,6 +117,7 @@ class ImageChatWidget extends StatelessWidget {
Icons.image, Icons.image,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: DownloadButton( extra: DownloadButton(
onPressed: () { onPressed: () {

View File

@ -1,7 +1,7 @@
import 'package:better_open_file/better_open_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxxyv2/shared/models/media.dart'; import 'package:moxxyv2/shared/models/media.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/media/audio.dart'; import 'package:moxxyv2/ui/widgets/chat/media/audio.dart';
import 'package:moxxyv2/ui/widgets/chat/media/file.dart'; import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
import 'package:moxxyv2/ui/widgets/chat/media/image.dart'; import 'package:moxxyv2/ui/widgets/chat/media/image.dart';
@ -84,7 +84,7 @@ Widget buildMessageWidget(Message message, double maxWidth, BorderRadius radius,
case MessageType.audio: case MessageType.audio:
return AudioChatWidget(message, radius, maxWidth, sent); return AudioChatWidget(message, radius, maxWidth, sent);
case MessageType.file: { case MessageType.file: {
return FileChatWidget(message, radius, sent); return FileChatWidget(message, radius, maxWidth, sent);
} }
} }
} }
@ -116,25 +116,25 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
if (medium.mime!.startsWith('image/')) { if (medium.mime!.startsWith('image/')) {
return SharedImageWidget( return SharedImageWidget(
medium.path, medium.path,
onTap: () => OpenFile.open(medium.path), onTap: () => openFile(medium.path),
); );
} else if (medium.mime!.startsWith('video/')) { } else if (medium.mime!.startsWith('video/')) {
return SharedVideoWidget( return SharedVideoWidget(
medium.path, medium.path,
conversationJid, conversationJid,
medium.mime!, medium.mime!,
onTap: () => OpenFile.open(medium.path), onTap: () => openFile(medium.path),
child: const PlayButton(size: 32), child: const PlayButton(size: 32),
); );
} else if (medium.mime!.startsWith('audio/')) { } else if (medium.mime!.startsWith('audio/')) {
return SharedAudioWidget( return SharedAudioWidget(
medium.path, medium.path,
onTap: () => OpenFile.open(medium.path), onTap: () => openFile(medium.path),
); );
} }
return SharedFileWidget( return SharedFileWidget(
medium.path, medium.path,
onTap: () => OpenFile.open(medium.path), onTap: () => openFile(medium.path),
); );
} }

View File

@ -1,11 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:better_open_file/better_open_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/bottom.dart'; import 'package:moxxyv2/ui/widgets/chat/bottom.dart';
import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart'; import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart';
import 'package:moxxyv2/ui/widgets/chat/helpers.dart'; import 'package:moxxyv2/ui/widgets/chat/helpers.dart';
@ -72,6 +72,7 @@ class VideoChatWidget extends StatelessWidget {
Icons.video_file_outlined, Icons.video_file_outlined,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: ProgressWidget(id: message.id), extra: ProgressWidget(id: message.id),
); );
@ -93,9 +94,7 @@ class VideoChatWidget extends StatelessWidget {
), ),
MessageBubbleBottom(message, sent), MessageBubbleBottom(message, sent),
radius, radius,
onTap: () { onTap: () => openFile(message.mediaUrl!),
OpenFile.open(message.mediaUrl);
},
extra: const PlayButton(), extra: const PlayButton(),
); );
} }
@ -126,6 +125,7 @@ class VideoChatWidget extends StatelessWidget {
Icons.video_file_outlined, Icons.video_file_outlined,
message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!), message.isFileUploadNotification ? (message.filename ?? '') : filenameFromUrl(message.srcUrl!),
radius, radius,
maxWidth,
sent, sent,
extra: DownloadButton( extra: DownloadButton(
onPressed: () { onPressed: () {

View File

@ -45,11 +45,9 @@ class QuoteBaseWidget extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Container( child: Material(
decoration: BoxDecoration( color: _getColor(),
color: _getColor(), borderRadius: const BorderRadius.all(radiusLarge),
borderRadius: const BorderRadius.all(radiusLarge),
),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Stack( child: Stack(
children: [ children: [
@ -62,19 +60,19 @@ class QuoteBaseWidget extends StatelessWidget {
width: quoteLeftBorderWidth, width: quoteLeftBorderWidth,
), ),
), ),
...resetQuotedMessage != null ? [
if (resetQuotedMessage != null)
Positioned( Positioned(
right: 3, right: 3,
top: 3, top: 3,
child: InkWell( child: IconButton(
onTap: resetQuotedMessage, onPressed: resetQuotedMessage,
child: const Icon( icon: const Icon(
Icons.close, Icons.close,
size: 24, size: 24,
), ),
), ),
) ),
] : [],
Padding( Padding(
padding: padding, padding: padding,
child: child, child: child,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart'; import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
class SharedAudioWidget extends StatelessWidget { class SharedAudioWidget extends StatelessWidget {
@ -23,7 +24,6 @@ class SharedAudioWidget extends StatelessWidget {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
color: Colors.white60,
border: borderColor != null ? Border.all( border: borderColor != null ? Border.all(
color: borderColor!, color: borderColor!,
width: 4, width: 4,
@ -35,6 +35,7 @@ class SharedAudioWidget extends StatelessWidget {
size: size * 2/3, size: size * 2/3,
), ),
), ),
color: sharedMediaItemBackgroundColor,
size: size, size: size,
onTap: onTap, onTap: onTap,
); );

View File

@ -24,14 +24,17 @@ const sharedMediaContainerDimension = 75.0;
/// A widget to show a message that was sent within a chat or is about to be sent. /// A widget to show a message that was sent within a chat or is about to be sent.
class SharedMediaContainer extends StatelessWidget { class SharedMediaContainer extends StatelessWidget {
const SharedMediaContainer(this.child, { const SharedMediaContainer(this.child, {
this.onTap, this.onTap,
this.size = sharedMediaContainerDimension, this.size = sharedMediaContainerDimension,
super.key, this.borderRadius = 10,
} required this.color,
); super.key,
});
final double borderRadius;
final Widget? child; final Widget? child;
final void Function()? onTap; final void Function()? onTap;
final double size; final double size;
final Color color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -44,13 +47,17 @@ class SharedMediaContainer extends StatelessWidget {
), ),
); );
if (onTap != null) { return ClipRRect(
return InkWell( borderRadius: BorderRadius.circular(borderRadius),
onTap: onTap, child: Material(
child: childWidget, color: color,
); child: onTap != null ?
} InkWell(
onTap: onTap,
return childWidget; child: childWidget,
) :
childWidget,
),
);
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart'; import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
class SharedFileWidget extends StatelessWidget { class SharedFileWidget extends StatelessWidget {
@ -18,16 +19,12 @@ class SharedFileWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SharedMediaContainer( return SharedMediaContainer(
DecoratedBox( Icon(
decoration: BoxDecoration( Icons.file_present,
borderRadius: BorderRadius.circular(borderRadius), size: size * 2/3,
color: Colors.white60,
),
child: Icon(
Icons.file_present,
size: size * 2/3,
),
), ),
color: sharedMediaItemBackgroundColor,
borderRadius: borderRadius,
size: size, size: size,
onTap: onTap, onTap: onTap,
); );

View File

@ -38,6 +38,8 @@ class SharedImageWidget extends StatelessWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: child, child: child,
), ),
borderRadius: borderRadius,
color: Colors.transparent,
size: size, size: size,
onTap: onTap, onTap: onTap,
); );

View File

@ -11,20 +11,15 @@ class SharedSummaryWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final number = min(notShown, 99); final number = min(notShown, 99);
return SharedMediaContainer( return SharedMediaContainer(
ClipRRect( Center(
borderRadius: BorderRadius.circular(10), child: Text(
child: ColoredBox( '+$number',
color: Colors.black38, style: const TextStyle(
child: Center( fontSize: 30,
child: Text(
'+$number',
style: const TextStyle(
fontSize: 30,
),
),
), ),
), ),
), ),
color: sharedMediaSummaryBackgroundColor,
onTap: () => Navigator.of(context).pushNamed(sharedMediaRoute), onTap: () => Navigator.of(context).pushNamed(sharedMediaRoute),
); );
} }

View File

@ -37,6 +37,8 @@ class SharedVideoWidget extends StatelessWidget {
), ),
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
), ),
borderRadius: borderRadius,
color: Colors.transparent,
size: size, size: size,
onTap: onTap, onTap: onTap,
); );

View File

View File

@ -1,9 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/models/sticker.dart'; import 'package:moxxyv2/shared/models/sticker.dart';
import 'package:moxxyv2/shared/models/sticker_pack.dart'; import 'package:moxxyv2/shared/models/sticker_pack.dart';
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart' as nav;
import 'package:moxxyv2/ui/bloc/sticker_pack_bloc.dart';
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart'; import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
import 'package:moxxyv2/ui/constants.dart';
class StickerPicker extends StatelessWidget { class StickerPicker extends StatelessWidget {
StickerPicker({ StickerPicker({
@ -18,13 +23,114 @@ class StickerPicker extends StatelessWidget {
late final double _itemSize; late final double _itemSize;
final void Function(Sticker, StickerPack) onStickerTapped; final void Function(Sticker, StickerPack) onStickerTapped;
Widget _buildList(BuildContext context, StickersState state) {
// TODO(PapaTutuWawa): Solve this somewhere else
final stickerPacks = state.stickerPacks
.where((pack) => !pack.restricted)
.toList();
if (stickerPacks.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${t.pages.conversation.stickerPickerNoStickersLine1}\n${t.pages.conversation.stickerPickerNoStickersLine2}',
textAlign: TextAlign.center,
),
TextButton(
onPressed: () {
context.read<nav.NavigationBloc>().add(
nav.PushedNamedEvent(
const nav.NavigationDestination(stickersRoute),
),
);
},
child: Text(t.pages.conversation.stickerSettings),
),
],
),
),
);
}
return ListView.builder(
itemCount: stickerPacks.length * 2,
itemBuilder: (_, si) {
if (si.isEven) {
return Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
stickerPacks[si ~/ 2].name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 20,
),
),
);
}
final sindex = (si - 1) ~/ 2;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: (stickerPacks[sindex].stickers.length / 4).ceil(),
itemBuilder: (_, index) {
final stickersLength = stickerPacks[sindex].stickers.length - index * 4;
return SizedBox(
width: width,
child: Row(
children: List<int>.generate(
stickersLength >= 4 ?
4 :
stickersLength,
(i) => i,
).map((rowIndex) {
return Padding(
padding: const EdgeInsets.all(15),
child: InkWell(
onTap: () {
onStickerTapped(
stickerPacks[sindex].stickers[index * 4 + rowIndex],
stickerPacks[sindex],
);
},
onLongPress: () {
Vibrate.feedback(FeedbackType.medium);
context.read<StickerPackBloc>().add(
LocallyAvailableStickerPackRequested(
stickerPacks[sindex].id,
),
);
},
child: Image.file(
File(
stickerPacks[sindex].stickers[index * 4 + rowIndex].path,
),
key: ValueKey('${state.stickerPacks[sindex].id}_${index * 4 + rowIndex}'),
fit: BoxFit.contain,
width: _itemSize,
height: _itemSize,
),
),
);
}).toList(),
),
);
},
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<StickersBloc, StickersState>( return BlocBuilder<StickersBloc, StickersState>(
builder: (context, state) { builder: (context, state) {
final stickerPacks = state.stickerPacks
.where((pack) => !pack.restricted)
.toList();
return SizedBox( return SizedBox(
height: 250, height: 250,
@ -33,66 +139,7 @@ class StickerPicker extends StatelessWidget {
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 16), padding: const EdgeInsets.only(top: 16),
child: ListView.builder( child: _buildList(context, state),
itemCount: stickerPacks.length * 2,
itemBuilder: (_, si) {
if (si.isEven) {
return Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
stickerPacks[si ~/ 2].name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 20,
),
),
);
}
final sindex = (si - 1) ~/ 2;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: (stickerPacks[sindex].stickers.length / 4).ceil(),
itemBuilder: (_, index) {
final stickersLength = stickerPacks[sindex].stickers.length - index * 4;
return SizedBox(
width: width,
child: Row(
children: List<int>.generate(
stickersLength >= 4 ?
4 :
stickersLength,
(i) => i,
).map((rowIndex) {
return Padding(
padding: const EdgeInsets.all(15),
child: InkWell(
onTap: () {
onStickerTapped(
stickerPacks[sindex].stickers[index * 4 + rowIndex],
stickerPacks[sindex],
);
},
child: Image.file(
File(
stickerPacks[sindex].stickers[index * 4 + rowIndex].path,
),
key: ValueKey('${state.stickerPacks[sindex].id}_${index * 4 + rowIndex}'),
fit: BoxFit.contain,
width: _itemSize,
height: _itemSize,
),
),
);
}).toList(),
),
);
},
);
},
),
), ),
), ),
); );

View File

@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
auto_size_text:
dependency: "direct main"
description:
name: auto_size_text
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
awesome_notifications: awesome_notifications:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -12,6 +12,7 @@ environment:
dependencies: dependencies:
archive: 3.3.0 archive: 3.3.0
audiofileplayer: 2.1.1 audiofileplayer: 2.1.1
auto_size_text: 3.0.0
awesome_notifications: 0.7.4+1 awesome_notifications: 0.7.4+1
badges: 2.0.3 badges: 2.0.3
better_open_file: 3.6.3 better_open_file: 3.6.3