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"
},
"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": {
@ -127,7 +129,10 @@
"showWarning": "Show warning",
"addToContacts": "Add 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": {
"title": "Add new contact",

View File

@ -73,7 +73,9 @@
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
},
"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": {
@ -127,7 +129,10 @@
"showWarning": "Warnung anzeigen",
"addToContacts": "Zu Kontaken 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": {
"title": "Neuen Kontakt hinzufügen",

View File

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

View File

@ -37,6 +37,11 @@ const double fontsizeBody = 15;
const double fontsizeBodyOnlyEmojis = 30;
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
// displaying the download progress indicator.
final backdropBlack = Colors.black.withAlpha(150);

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:better_open_file/better_open_file.dart';
import 'package:cryptography/cryptography.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@ -353,3 +354,24 @@ Future<void> handleUri(String uriString) async {
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(
right: 8,
bottom: 80,
child: Material(
color: const Color.fromRGBO(0, 0, 0, 0),
child: ScaleTransition(
scale: _scrollToBottom,
alignment: FractionalOffset.center,
child: SizedBox(
width: 45,
height: 45,
child: FloatingActionButton(
heroTag: 'fabScrollDown',
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
onPressed: () {
_scrollController.jumpTo(0);
},
child: const Icon(
Icons.arrow_downward,
// TODO(Unknown): Theme dependent
color: Colors.white,
BlocBuilder<ConversationBloc, ConversationState>(
buildWhen: (prev, next) => prev.emojiPickerVisible != next.emojiPickerVisible ||
prev.stickerPickerVisible != next.stickerPickerVisible,
builder: (context, state) => Positioned(
right: 8,
bottom: state.emojiPickerVisible || state.stickerPickerVisible ?
330 /* 80 + 250 */ :
80,
child: Material(
color: const Color.fromRGBO(0, 0, 0, 0),
child: ScaleTransition(
scale: _scrollToBottom,
alignment: FractionalOffset.center,
child: SizedBox(
width: 45,
height: 45,
child: FloatingActionButton(
heroTag: 'fabScrollDown',
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
onPressed: () {
_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,
children: [
SharedMediaContainer(
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: ColoredBox(
color: getTileColor(context),
child: Icon(
conversation.muted ?
Icons.do_not_disturb_on :
Icons.do_not_disturb_off,
size: 32,
),
),
Icon(
conversation.muted ?
Icons.do_not_disturb_on :
Icons.do_not_disturb_off,
size: 32,
),
color: getTileColor(context),
onTap: () {
GetIt.I.get<ProfileBloc>().add(
MuteStateSetEvent(
@ -134,16 +129,11 @@ class ConversationProfileHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SharedMediaContainer(
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: ColoredBox(
color: getTileColor(context),
child: const Icon(
Icons.security_outlined,
size: 32,
),
),
const Icon(
Icons.security_outlined,
size: 32,
),
color: getTileColor(context),
onTap: () {
GetIt.I.get<DevicesBloc>().add(DevicesRequestedEvent(conversation.jid));
},

View File

@ -90,16 +90,11 @@ class SelfProfileHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SharedMediaContainer(
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: ColoredBox(
color: getTileColor(context),
child: const Icon(
Icons.security_outlined,
size: 32,
),
),
const Icon(
Icons.security_outlined,
size: 32,
),
color: getTileColor(context),
onTap: () {
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: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 {
const FingerprintListItem(
this.fingerprint,
@ -26,14 +36,8 @@ class FingerprintListItem extends StatelessWidget {
@override
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 fontSize = width * 0.04;
final fontSize = width * 0.1;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
@ -47,17 +51,25 @@ class FingerprintListItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 6,
children: parts
.map((part_) => Text(
part_,
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: fontSize,
),
),).toList(),
AutoSizeText(
_formatHalfFingerprint(fingerprint.substring(0, 32)),
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: fontSize,
),
textAlign: TextAlign.center,
maxLines: 1,
),
AutoSizeText(
_formatHalfFingerprint(fingerprint.substring(32)),
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: fontSize,
),
textAlign: TextAlign.center,
maxLines: 1,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import 'package:better_open_file/better_open_file.dart';
import 'package:flutter/material.dart';
import 'package:moxxyv2/shared/models/media.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/file.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:
return AudioChatWidget(message, radius, maxWidth, sent);
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/')) {
return SharedImageWidget(
medium.path,
onTap: () => OpenFile.open(medium.path),
onTap: () => openFile(medium.path),
);
} else if (medium.mime!.startsWith('video/')) {
return SharedVideoWidget(
medium.path,
conversationJid,
medium.mime!,
onTap: () => OpenFile.open(medium.path),
onTap: () => openFile(medium.path),
child: const PlayButton(size: 32),
);
} else if (medium.mime!.startsWith('audio/')) {
return SharedAudioWidget(
medium.path,
onTap: () => OpenFile.open(medium.path),
onTap: () => openFile(medium.path),
);
}
return SharedFileWidget(
medium.path,
onTap: () => OpenFile.open(medium.path),
onTap: () => openFile(medium.path),
);
}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
class SharedAudioWidget extends StatelessWidget {
@ -23,7 +24,6 @@ class SharedAudioWidget extends StatelessWidget {
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
color: Colors.white60,
border: borderColor != null ? Border.all(
color: borderColor!,
width: 4,
@ -35,6 +35,7 @@ class SharedAudioWidget extends StatelessWidget {
size: size * 2/3,
),
),
color: sharedMediaItemBackgroundColor,
size: size,
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.
class SharedMediaContainer extends StatelessWidget {
const SharedMediaContainer(this.child, {
this.onTap,
this.size = sharedMediaContainerDimension,
super.key,
}
);
this.onTap,
this.size = sharedMediaContainerDimension,
this.borderRadius = 10,
required this.color,
super.key,
});
final double borderRadius;
final Widget? child;
final void Function()? onTap;
final double size;
final Color color;
@override
Widget build(BuildContext context) {
@ -44,13 +47,17 @@ class SharedMediaContainer extends StatelessWidget {
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
child: childWidget,
);
}
return childWidget;
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: Material(
color: color,
child: onTap != null ?
InkWell(
onTap: onTap,
child: childWidget,
) :
childWidget,
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

View File

@ -1,9 +1,14 @@
import 'dart:io';
import 'package:flutter/material.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_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/constants.dart';
class StickerPicker extends StatelessWidget {
StickerPicker({
@ -18,13 +23,114 @@ class StickerPicker extends StatelessWidget {
late final double _itemSize;
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
Widget build(BuildContext context) {
return BlocBuilder<StickersBloc, StickersState>(
builder: (context, state) {
final stickerPacks = state.stickerPacks
.where((pack) => !pack.restricted)
.toList();
return SizedBox(
height: 250,
@ -33,66 +139,7 @@ class StickerPicker extends StatelessWidget {
color: Theme.of(context).scaffoldBackgroundColor,
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: 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],
);
},
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(),
),
);
},
);
},
),
child: _buildList(context, state),
),
),
);

View File

@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:

View File

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