Merge branch 'main' into fix-jagged-avatar-edges

pull/1778/head
Martin Wege 3 months ago committed by GitHub
commit c5b61aba64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,26 @@
name: Matrix Notification
on:
issues:
types: [ opened ]
issue_comment:
types: [ created ]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Matrix Notification
env:
MATRIX_URL: https://matrix.janian.de/_matrix/client/v3/rooms/${{ secrets.MATRIX_MANAGEMENT_ROOM }}/send/m.room.message
run: |
if [ "${{ github.event.action }}" == "opened" ]; then
PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Issue from ${{ github.event.issue.user.login }}\\n${{ github.event.issue.title }}\\n\\n${{ github.event.issue.body }}\\n\\nURL: ${{ github.event.issue.html_url }}\"}"
elif [ "${{ github.event.action }}" == "created" ]; then
PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Comment from ${{ github.event.comment.user.login }}\\n\\n${{ github.event.comment.body }}\\n\\nURL: ${{ github.event.comment.html_url }}\"}"
fi
curl -X POST -H "Authorization: Bearer ${{ secrets.MATRIX_BOT_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
$MATRIX_URL

@ -1,2 +1,2 @@
FLUTTER_VERSION=3.29.2 FLUTTER_VERSION=3.29.3
JAVA_VERSION=17 JAVA_VERSION=17

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
# Privacy # Privacy
FluffyChat is available on Android, iOS and as a web version. Desktop versions for Windows, Linux and macOS may follow. FluffyChat is available on Android, iOS, Linux and as a web version. Desktop versions for Windows and macOS may follow.
* [Matrix](#matrix) * [Matrix](#matrix)
* [Database](#database) * [Database](#database)
@ -109,4 +109,4 @@ To enhance user safety and help protect against the sexual abuse and exploitatio
In addition to reporting messages, users can also report other users following a similar process. In addition to reporting messages, users can also report other users following a similar process.
We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/ We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/

@ -3211,5 +3211,8 @@
"recordAVideo": "Record a video", "recordAVideo": "Record a video",
"optionalMessage": "(Optional) message...", "optionalMessage": "(Optional) message...",
"notSupportedOnThisDevice": "Not supported on this device", "notSupportedOnThisDevice": "Not supported on this device",
"enterNewChat": "Enter new chat" "enterNewChat": "Enter new chat",
"approve": "Approve",
"youHaveKnocked": "You have knocked",
"pleaseWaitUntilInvited": "Please wait now, until someone from the room invites you."
} }

@ -75,6 +75,14 @@ abstract class FluffyThemes {
), ),
contentPadding: const EdgeInsets.all(12), contentPadding: const EdgeInsets.all(12),
), ),
chipTheme: ChipThemeData(
showCheckmark: false,
backgroundColor: colorScheme.surfaceContainer,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
toolbarHeight: isColumnMode ? 72 : 56, toolbarHeight: isColumnMode ? 72 : 56,
shadowColor: shadowColor:

@ -18,6 +18,7 @@ import '../key_verification/key_verification_dialog.dart';
class BootstrapDialog extends StatefulWidget { class BootstrapDialog extends StatefulWidget {
final bool wipe; final bool wipe;
final Client client; final Client client;
const BootstrapDialog({ const BootstrapDialog({
super.key, super.key,
this.wipe = false, this.wipe = false,
@ -132,7 +133,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
minLines: 2, minLines: 2,
maxLines: 4, maxLines: 4,
readOnly: true, readOnly: true,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
controller: TextEditingController(text: key), controller: TextEditingController(text: key),
decoration: const InputDecoration( decoration: const InputDecoration(
contentPadding: EdgeInsets.all(16), contentPadding: EdgeInsets.all(16),
@ -257,7 +258,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
? null ? null
: [AutofillHints.password], : [AutofillHints.password],
controller: _recoveryKeyTextEditingController, controller: _recoveryKeyTextEditingController,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle( hintStyle: TextStyle(

@ -37,6 +37,7 @@ class ChatAppBarListTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Linkify( child: Linkify(
text: title, text: title,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

@ -21,6 +21,7 @@ extension EventInfoDialogExtension on Event {
class EventInfoDialog extends StatelessWidget { class EventInfoDialog extends StatelessWidget {
final Event event; final Event event;
final L10n l10n; final L10n l10n;
const EventInfoDialog({ const EventInfoDialog({
required this.event, required this.event,
required this.l10n, required this.l10n,
@ -41,10 +42,8 @@ class EventInfoDialog extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).messageInfo), title: Text(L10n.of(context).messageInfo),
leading: IconButton( leading: CloseButton(
icon: const Icon(Icons.arrow_downward_outlined),
onPressed: Navigator.of(context, rootNavigator: false).pop, onPressed: Navigator.of(context, rootNavigator: false).pop,
tooltip: L10n.of(context).close,
), ),
), ),
body: ListView( body: ListView(

@ -395,20 +395,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
), ),
if (fileDescription != null) ...[ if (fileDescription != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Linkify( Padding(
text: fileDescription, padding: const EdgeInsets.symmetric(
style: TextStyle( horizontal: 16,
color: widget.color, vertical: 8,
fontSize: widget.fontSize,
), ),
options: const LinkifyOptions(humanize: false), child: Linkify(
linkStyle: TextStyle( text: fileDescription,
color: widget.linkColor, textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
fontSize: widget.fontSize, style: TextStyle(
decoration: TextDecoration.underline, color: widget.color,
decorationColor: widget.linkColor, fontSize: widget.fontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: widget.linkColor,
fontSize: widget.fontSize,
decoration: TextDecoration.underline,
decorationColor: widget.linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
], ],
], ],
@ -420,6 +427,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
/// To use a MatrixFile as an AudioSource for the just_audio package /// To use a MatrixFile as an AudioSource for the just_audio package
class MatrixFileAudioSource extends StreamAudioSource { class MatrixFileAudioSource extends StreamAudioSource {
final MatrixFile file; final MatrixFile file;
MatrixFileAudioSource(this.file); MatrixFileAudioSource(this.file);
@override @override

@ -250,7 +250,7 @@ class HtmlMessage extends StatelessWidget {
border: Border( border: Border(
left: BorderSide( left: BorderSide(
color: textColor, color: textColor,
width: 3, width: 5,
), ),
), ),
), ),
@ -295,7 +295,7 @@ class HtmlMessage extends StatelessWidget {
), ),
textStyle: TextStyle( textStyle: TextStyle(
fontSize: fontSize, fontSize: fontSize,
fontFamily: 'UbuntuMono', fontFamily: 'RobotoMono',
), ),
), ),
), ),

@ -79,12 +79,19 @@ class ImageBubble extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final borderRadius = var borderRadius =
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
final fileDescription = event.fileDescription; final fileDescription = event.fileDescription;
final textColor = this.textColor; final textColor = this.textColor;
if (fileDescription != null) {
borderRadius = borderRadius.copyWith(
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
);
}
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
@ -122,20 +129,29 @@ class ImageBubble extends StatelessWidget {
if (fileDescription != null && textColor != null) if (fileDescription != null && textColor != null)
SizedBox( SizedBox(
width: width, width: width,
child: Linkify( child: Padding(
text: fileDescription, padding: const EdgeInsets.symmetric(
style: TextStyle( horizontal: 16,
color: textColor, vertical: 8,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), child: Linkify(
linkStyle: TextStyle( text: fileDescription,
color: linkColor, textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, style: TextStyle(
decoration: TextDecoration.underline, color: textColor,
decorationColor: linkColor, fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
), ),
], ],

@ -20,7 +20,6 @@ import 'message_content.dart';
import 'message_reactions.dart'; import 'message_reactions.dart';
import 'reply_content.dart'; import 'reply_content.dart';
import 'state_message.dart'; import 'state_message.dart';
import 'verification_request_content.dart';
class Message extends StatelessWidget { class Message extends StatelessWidget {
final Event event; final Event event;
@ -85,7 +84,7 @@ class Message extends StatelessWidget {
if (event.type == EventTypes.Message && if (event.type == EventTypes.Message &&
event.messageType == EventTypes.KeyVerificationRequest) { event.messageType == EventTypes.KeyVerificationRequest) {
return VerificationRequestContent(event: event, timeline: timeline); return StateMessage(event);
} }
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
@ -149,10 +148,6 @@ class Message extends StatelessWidget {
event.onlyEmotes && event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 3); event.numberEmotes <= 3);
final noPadding = {
MessageTypes.File,
MessageTypes.Audio,
}.contains(event.messageType);
if (ownMessage) { if (ownMessage) {
color = color =
@ -338,12 +333,6 @@ class Message extends StatelessWidget {
AppConfig.borderRadius, AppConfig.borderRadius,
), ),
), ),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: maxWidth:
FluffyThemes.columnWidth * 1.5, FluffyThemes.columnWidth * 1.5,
@ -386,22 +375,32 @@ class Message extends StatelessWidget {
return Padding( return Padding(
padding: padding:
const EdgeInsets.only( const EdgeInsets.only(
bottom: 4.0, left: 16,
right: 16,
top: 8,
), ),
child: InkWell( child: Material(
color:
Colors.transparent,
borderRadius: borderRadius:
ReplyContent ReplyContent
.borderRadius, .borderRadius,
onTap: () => child: InkWell(
scrollToEventId( borderRadius:
replyEvent.eventId, ReplyContent
), .borderRadius,
child: AbsorbPointer( onTap: () =>
child: ReplyContent( scrollToEventId(
replyEvent, replyEvent.eventId,
ownMessage: ),
ownMessage, child: AbsorbPointer(
timeline: timeline, child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
), ),
), ),
), ),

@ -185,19 +185,26 @@ class MessageContent extends StatelessWidget {
if (event.messageType == MessageTypes.Emote) { if (event.messageType == MessageTypes.Emote) {
html = '* $html'; html = '* $html';
} }
return HtmlMessage( return Padding(
html: html, padding: const EdgeInsets.symmetric(
textColor: textColor, horizontal: 16,
room: event.room, vertical: 8,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, ),
linkStyle: TextStyle( child: HtmlMessage(
color: linkColor, html: html,
textColor: textColor,
room: event.room,
fontSize: fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize, AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline, linkStyle: TextStyle(
decorationColor: linkColor, color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
); );
} }
// else we fall through to the normal message rendering // else we fall through to the normal message rendering
@ -276,24 +283,32 @@ class MessageContent extends StatelessWidget {
final bigEmotes = event.onlyEmotes && final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 3; event.numberEmotes <= 3;
return Linkify( return Padding(
text: event.calcLocalizedBodyFallback( padding: const EdgeInsets.symmetric(
MatrixLocals(L10n.of(context)), horizontal: 16,
hideReply: true, vertical: 8,
),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 5 : fontSize,
decoration: event.redacted ? TextDecoration.lineThrough : null,
), ),
options: const LinkifyOptions(humanize: false), child: Linkify(
linkStyle: TextStyle( text: event.calcLocalizedBodyFallback(
color: linkColor, MatrixLocals(L10n.of(context)),
fontSize: fontSize, hideReply: true,
decoration: TextDecoration.underline, ),
decorationColor: linkColor, textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 5 : fontSize,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
); );
} }
case EventTypes.CallInvite: case EventTypes.CallInvite:
@ -350,13 +365,19 @@ class _ButtonContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Padding(
onTap: onPressed, padding: const EdgeInsets.symmetric(
child: Text( horizontal: 16,
'$icon $label', vertical: 8,
style: TextStyle( ),
color: textColor, child: InkWell(
fontSize: fontSize, onTap: onPressed,
child: Text(
'$icon $label',
style: TextStyle(
color: textColor,
fontSize: fontSize,
),
), ),
), ),
); );

@ -1,12 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/url_launcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
class MessageDownloadContent extends StatelessWidget { class MessageDownloadContent extends StatelessWidget {
final Event event; final Event event;
@ -30,83 +28,79 @@ class MessageDownloadContent extends StatelessWidget {
?.tryGet<String>('mimetype') ?.tryGet<String>('mimetype')
?.toUpperCase() ?? ?.toUpperCase() ??
'UNKNOWN'); 'UNKNOWN');
final sizeString = event.sizeString; final sizeString = event.sizeString ?? '?MB';
final fileDescription = event.fileDescription; final fileDescription = event.fileDescription;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8, spacing: 8,
children: [ children: [
InkWell( Material(
onTap: () => event.saveFile(context), color: Colors.transparent,
child: Column( child: InkWell(
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
mainAxisSize: MainAxisSize.min, onTap: () => event.saveFile(context),
children: <Widget>[ child: Container(
Padding( width: 400,
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row( child: Row(
children: [ mainAxisSize: MainAxisSize.min,
Icon( spacing: 16,
Icons.file_download_outlined, children: [
color: textColor, CircleAvatar(
), backgroundColor: textColor.withAlpha(32),
const SizedBox(width: 16), child: Icon(Icons.file_download_outlined, color: textColor),
Flexible( ),
child: Text( Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
filename, filename,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w500,
), ),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const Divider(height: 1),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row(
children: [
Text(
filetype,
style: TextStyle(
color: linkColor,
), ),
),
const Spacer(),
if (sizeString != null)
Text( Text(
sizeString, '$sizeString | $filetype',
style: TextStyle( maxLines: 1,
color: linkColor, overflow: TextOverflow.ellipsis,
), style: TextStyle(color: textColor, fontSize: 10),
), ),
], ],
), ),
],
), ),
], ),
), ),
), ),
if (fileDescription != null) if (fileDescription != null) ...[
Linkify( Padding(
text: fileDescription, padding: const EdgeInsets.symmetric(
style: TextStyle( horizontal: 16.0,
color: textColor, vertical: 8.0,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), child: Linkify(
linkStyle: TextStyle( text: fileDescription,
color: linkColor, textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, style: TextStyle(
decoration: TextDecoration.underline, color: textColor,
decorationColor: linkColor, fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
],
], ],
); );
} }

@ -10,14 +10,12 @@ class ReplyContent extends StatelessWidget {
final Event replyEvent; final Event replyEvent;
final bool ownMessage; final bool ownMessage;
final Timeline? timeline; final Timeline? timeline;
final Color? backgroundColor;
const ReplyContent( const ReplyContent(
this.replyEvent, { this.replyEvent, {
this.ownMessage = false, this.ownMessage = false,
super.key, super.key,
this.timeline, this.timeline,
this.backgroundColor,
}); });
static const BorderRadius borderRadius = BorderRadius.only( static const BorderRadius borderRadius = BorderRadius.only(
@ -40,16 +38,18 @@ class ReplyContent extends StatelessWidget {
: theme.colorScheme.tertiary; : theme.colorScheme.tertiary;
return Material( return Material(
color: backgroundColor ?? color: Colors.transparent,
theme.colorScheme.surface.withAlpha(ownMessage ? 50 : 80),
borderRadius: borderRadius, borderRadius: borderRadius,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container( Container(
width: 3, width: 5,
height: fontSize * 2 + 16, height: fontSize * 2 + 16,
color: color, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: color,
),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../../config/app_config.dart';
class VerificationRequestContent extends StatelessWidget {
final Event event;
final Timeline timeline;
const VerificationRequestContent({
required this.event,
required this.timeline,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final events = event.aggregatedEvents(timeline, 'm.reference');
final done = events.where((e) => e.type == EventTypes.KeyVerificationDone);
final start =
events.where((e) => e.type == EventTypes.KeyVerificationStart);
final cancel =
events.where((e) => e.type == EventTypes.KeyVerificationCancel);
final fullyDone = done.length >= 2;
final started = start.isNotEmpty;
final canceled = cancel.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Center(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor,
),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: theme.colorScheme.surface,
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(
Icons.lock_outlined,
color: canceled
? Colors.red
: (fullyDone ? Colors.green : Colors.grey),
),
const SizedBox(width: 8),
Text(
canceled
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
: (fullyDone
? L10n.of(context).verifySuccess
: (started
? L10n.of(context).loadingPleaseWait
: L10n.of(context).newVerificationRequest)),
),
],
),
),
),
);
}
}

@ -25,6 +25,7 @@ class EventVideoPlayer extends StatefulWidget {
final Event event; final Event event;
final Color? textColor; final Color? textColor;
final Color? linkColor; final Color? linkColor;
const EventVideoPlayer( const EventVideoPlayer(
this.event, { this.event, {
this.textColor, this.textColor,
@ -188,20 +189,29 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
if (fileDescription != null && textColor != null && linkColor != null) if (fileDescription != null && textColor != null && linkColor != null)
SizedBox( SizedBox(
width: width, width: width,
child: Linkify( child: Padding(
text: fileDescription, padding: const EdgeInsets.symmetric(
style: TextStyle( horizontal: 16,
color: textColor, vertical: 8,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), child: Linkify(
linkStyle: TextStyle( text: fileDescription,
color: linkColor, textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, style: TextStyle(
decoration: TextDecoration.underline, color: textColor,
decorationColor: linkColor, fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
), ),
], ],

@ -235,7 +235,7 @@ class InputBar extends StatelessWidget {
children: [ children: [
Text( Text(
commandExample(command), commandExample(command),
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
), ),
Text( Text(
hint, hint,
@ -255,7 +255,7 @@ class InputBar extends StatelessWidget {
waitDuration: const Duration(days: 1), // don't show on hover waitDuration: const Duration(days: 1), // don't show on hover
child: Container( child: Container(
padding: padding, padding: padding,
child: Text(label, style: const TextStyle(fontFamily: 'UbuntuMono')), child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
), ),
); );
} }

@ -38,7 +38,6 @@ class ReplyDisplay extends StatelessWidget {
? ReplyContent( ? ReplyContent(
controller.replyEvent!, controller.replyEvent!,
timeline: controller.timeline!, timeline: controller.timeline!,
backgroundColor: Colors.transparent,
) )
: _EditContent( : _EditContent(
controller.editEvent?.getDisplayEvent(controller.timeline!), controller.editEvent?.getDisplayEvent(controller.timeline!),

@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
import '../../widgets/mxc_image_viewer.dart';
import '../../widgets/qr_code_viewer.dart'; import '../../widgets/qr_code_viewer.dart';
class ChatDetailsView extends StatelessWidget { class ChatDetailsView extends StatelessWidget {
@ -38,6 +39,7 @@ class ChatDetailsView extends StatelessWidget {
} }
final directChatMatrixID = room.directChatMatrixID; final directChatMatrixID = room.directChatMatrixID;
final roomAvatar = room.avatar;
return StreamBuilder( return StreamBuilder(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream
@ -108,6 +110,13 @@ class ChatDetailsView extends StatelessWidget {
mxContent: room.avatar, mxContent: room.avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: roomAvatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(roomAvatar),
)
: null,
), ),
), ),
if (!room.isDirectChat && if (!room.isDirectChat &&
@ -234,6 +243,8 @@ class ChatDetailsView extends StatelessWidget {
text: room.topic.isEmpty text: room.topic.isEmpty
? L10n.of(context).noChatDescriptionYet ? L10n.of(context).noChatDescriptionYet
: room.topic, : room.topic,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle( linkStyle: const TextStyle(
color: Colors.blueAccent, color: Colors.blueAccent,

@ -30,65 +30,68 @@ class ParticipantListItem extends StatelessWidget {
? L10n.of(context).moderator ? L10n.of(context).moderator
: ''; : '';
return Opacity( return ListTile(
opacity: user.membership == Membership.join ? 1 : 0.5, onTap: () => showMemberActionsPopupMenu(context: context, user: user),
child: ListTile( title: Row(
onTap: () => showMemberActionsPopupMenu(context: context, user: user), children: <Widget>[
title: Row( Expanded(
children: <Widget>[ child: Text(
Expanded( user.calcDisplayname(),
child: Text( overflow: TextOverflow.ellipsis,
user.calcDisplayname(),
overflow: TextOverflow.ellipsis,
),
), ),
if (permissionBatch.isNotEmpty) ),
Container( if (permissionBatch.isNotEmpty)
padding: const EdgeInsets.symmetric( Container(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 6, horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: user.powerLevel >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
), ),
decoration: BoxDecoration( ),
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100 color: user.powerLevel >= 100
? theme.colorScheme.tertiary ? theme.colorScheme.onTertiary
: theme.colorScheme.tertiaryContainer, : theme.colorScheme.onTertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
), ),
), ),
membershipBatch == null ),
? const SizedBox.shrink() membershipBatch == null
: Container( ? const SizedBox.shrink()
padding: const EdgeInsets.all(4), : Container(
margin: const EdgeInsets.symmetric(horizontal: 8), padding:
decoration: BoxDecoration( const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
color: theme.secondaryHeaderColor, margin: const EdgeInsets.symmetric(horizontal: 8),
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
), color: theme.colorScheme.secondaryContainer,
child: Center( borderRadius: BorderRadius.circular(8),
child: Text( ),
membershipBatch, child: Center(
style: theme.textTheme.labelSmall, child: Text(
membershipBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
), ),
), ),
), ),
], ),
), ],
subtitle: Text( ),
user.id, subtitle: Text(
maxLines: 1, user.id,
overflow: TextOverflow.ellipsis, maxLines: 1,
), overflow: TextOverflow.ellipsis,
leading: Avatar( ),
leading: Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,
child: Avatar(
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
name: user.calcDisplayname(), name: user.calcDisplayname(),
presenceUserId: user.stateKey, presenceUserId: user.stateKey,

@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].ed25519Key?.beautified ?? deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context).unknownEncryptionAlgorithm, L10n.of(context).unknownEncryptionAlgorithm,
style: TextStyle( style: TextStyle(
fontFamily: 'UbuntuMono', fontFamily: 'RobotoMono',
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
), ),
), ),

@ -11,11 +11,9 @@ import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/chat_list/status_msg_list.dart'; import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
import '../../config/themes.dart'; import '../../config/themes.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart'; import '../../widgets/adaptive_dialogs/user_dialog.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
@ -155,7 +153,7 @@ class ChatListViewBody extends StatelessWidget {
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0, horizontal: 12.0,
vertical: 16.0, vertical: 12.0,
), ),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -172,53 +170,15 @@ class ChatListViewBody extends StatelessWidget {
] ]
.map( .map(
(filter) => Padding( (filter) => Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 4), horizontal: 4.0,
child: HoverBuilder( ),
builder: (context, hovered) => child: FilterChip(
AnimatedScale( selected: filter == controller.activeFilter,
duration: FluffyThemes.animationDuration, onSelected: (_) =>
curve: FluffyThemes.animationCurve, controller.setActiveFilter(filter),
scale: hovered ? 1.1 : 1.0, label:
child: InkWell( Text(filter.toLocalizedString(context)),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
onTap: () =>
controller.setActiveFilter(filter),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: filter ==
controller.activeFilter
? theme.colorScheme.primary
: theme.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
alignment: Alignment.center,
child: Text(
filter.toLocalizedString(context),
style: TextStyle(
fontWeight: filter ==
controller.activeFilter
? FontWeight.w500
: FontWeight.normal,
color: filter ==
controller.activeFilter
? theme.colorScheme.onPrimary
: theme.colorScheme
.onSecondaryContainer,
),
),
),
),
),
), ),
), ),
) )
@ -341,12 +301,11 @@ class PublicRoomsHorizontalList extends StatelessWidget {
publicRooms[i].canonicalAlias?.localpart ?? publicRooms[i].canonicalAlias?.localpart ??
L10n.of(context).group, L10n.of(context).group,
avatar: publicRooms[i].avatarUrl, avatar: publicRooms[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet( onPressed: () => showAdaptiveDialog(
context: context, context: context,
builder: (c) => PublicRoomBottomSheet( builder: (c) => PublicRoomDialog(
roomAlias: roomAlias:
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId, publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
outerContext: context,
chunk: publicRooms[i], chunk: publicRooms[i],
), ),
), ),

@ -10,16 +10,15 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
enum AddRoomType { chat, subspace } enum AddRoomType { chat, subspace }
@ -100,10 +99,9 @@ class _SpaceViewState extends State<SpaceView> {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final space = client.getRoomById(widget.spaceId); final space = client.getRoomById(widget.spaceId);
final joined = await showAdaptiveBottomSheet<bool>( final joined = await showAdaptiveDialog<bool>(
context: context, context: context,
builder: (_) => PublicRoomBottomSheet( builder: (_) => PublicRoomDialog(
outerContext: context,
chunk: item, chunk: item,
via: space?.spaceChildren via: space?.spaceChildren
.firstWhereOrNull( .firstWhereOrNull(

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -7,6 +9,7 @@ import 'chat_members_view.dart';
class ChatMembersPage extends StatefulWidget { class ChatMembersPage extends StatefulWidget {
final String roomId; final String roomId;
const ChatMembersPage({required this.roomId, super.key}); const ChatMembersPage({required this.roomId, super.key});
@override @override
@ -17,12 +20,27 @@ class ChatMembersController extends State<ChatMembersPage> {
List<User>? members; List<User>? members;
List<User>? filteredMembers; List<User>? filteredMembers;
Object? error; Object? error;
Membership membershipFilter = Membership.join;
final TextEditingController filterController = TextEditingController(); final TextEditingController filterController = TextEditingController();
void setMembershipFilter(Membership membership) {
membershipFilter = membership;
setFilter();
}
void setFilter([_]) async { void setFilter([_]) async {
final filter = filterController.text.toLowerCase().trim(); final filter = filterController.text.toLowerCase().trim();
final members = this
.members
?.where(
(member) =>
membershipFilter == Membership.join ||
member.membership == membershipFilter,
)
.toList();
if (filter.isEmpty) { if (filter.isEmpty) {
setState(() { setState(() {
filteredMembers = members filteredMembers = members
@ -42,7 +60,8 @@ class ChatMembersController extends State<ChatMembersPage> {
}); });
} }
void refreshMembers() async { void refreshMembers([_]) async {
Logs().d('Load room members from', widget.roomId);
try { try {
setState(() { setState(() {
error = null; error = null;
@ -50,7 +69,9 @@ class ChatMembersController extends State<ChatMembersPage> {
final participants = await Matrix.of(context) final participants = await Matrix.of(context)
.client .client
.getRoomById(widget.roomId) .getRoomById(widget.roomId)
?.requestParticipants(); ?.requestParticipants(
[...Membership.values]..remove(Membership.leave),
);
if (!mounted) return; if (!mounted) return;
@ -67,10 +88,30 @@ class ChatMembersController extends State<ChatMembersPage> {
} }
} }
StreamSubscription? _updateSub;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
refreshMembers(); refreshMembers();
_updateSub = Matrix.of(context)
.client
.onSync
.stream
.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[widget.roomId]?.timeline?.events
?.any((state) => state.type == EventTypes.RoomMember) ??
false,
)
.listen(refreshMembers);
}
@override
void dispose() {
_updateSub?.cancel();
super.dispose();
} }
@override @override

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import '../../widgets/layouts/max_width_body.dart'; import '../../widgets/layouts/max_width_body.dart';
@ -11,6 +12,7 @@ import 'chat_members.dart';
class ChatMembersView extends StatelessWidget { class ChatMembersView extends StatelessWidget {
final ChatMembersController controller; final ChatMembersController controller;
const ChatMembersView(this.controller, {super.key}); const ChatMembersView(this.controller, {super.key});
@override @override
@ -84,29 +86,89 @@ class ChatMembersView extends StatelessWidget {
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: members.length + 1, itemCount: members.length + 1,
itemBuilder: (context, i) => i == 0 itemBuilder: (context, i) {
? Padding( if (i == 0) {
padding: const EdgeInsets.all(16.0), final availableFilters = Membership.values
child: TextField( .where(
controller: controller.filterController, (membership) =>
onChanged: controller.setFilter, controller.members?.any(
decoration: InputDecoration( (member) => member.membership == membership,
filled: true, ) ??
fillColor: theme.colorScheme.secondaryContainer, false,
border: OutlineInputBorder( )
borderSide: BorderSide.none, .toList();
borderRadius: BorderRadius.circular(99), availableFilters
), .sort((a, b) => a == Membership.join ? -1 : 1);
hintStyle: TextStyle( return Column(
color: theme.colorScheme.onPrimaryContainer, mainAxisSize: MainAxisSize.min,
fontWeight: FontWeight.normal, children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: controller.filterController,
onChanged: controller.setFilter,
decoration: InputDecoration(
filled: true,
fillColor:
theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).search,
), ),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).search,
), ),
), ),
) if (availableFilters.length > 1)
: ParticipantListItem(members[i - 1]), SizedBox(
height: 64,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 12.0,
),
scrollDirection: Axis.horizontal,
itemCount: availableFilters.length,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
label: Text(
switch (availableFilters[i]) {
Membership.ban =>
L10n.of(context).banned,
Membership.invite =>
L10n.of(context).invited,
Membership.join =>
L10n.of(context).all,
Membership.knock =>
L10n.of(context).knocking,
Membership.leave =>
L10n.of(context).leftTheChat,
},
),
selected: controller.membershipFilter ==
availableFilters[i],
onSelected: (_) =>
controller.setMembershipFilter(
availableFilters[i],
),
),
),
),
),
],
);
}
i--;
return ParticipantListItem(members[i]);
},
), ),
), ),
); );

@ -152,6 +152,7 @@ class _MessageSearchResultListTile extends StatelessWidget {
], ],
), ),
subtitle: Linkify( subtitle: Linkify(
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,

@ -122,6 +122,8 @@ class HomeserverPickerView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 32.0), padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: SelectableLinkify( child: SelectableLinkify(
text: L10n.of(context).appIntroduction, text: L10n.of(context).appIntroduction,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
textAlign: TextAlign.center, textAlign: TextAlign.center,
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
@ -169,6 +171,19 @@ class HomeserverPickerView extends StatelessWidget {
content: Linkify( content: Linkify(
text: L10n.of(context) text: L10n.of(context)
.homeserverDescription, .homeserverDescription,
textScaleFactor:
MediaQuery.textScalerOf(context)
.scale(1),
options: const LinkifyOptions(
humanize: false,
),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
decorationColor:
theme.colorScheme.primary,
),
onOpen: (link) =>
launchUrlString(link.url),
), ),
actions: [ actions: [
AdaptiveDialogAction( AdaptiveDialogAction(

@ -12,6 +12,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/navigation_rail.dart'; import 'package:fluffychat/widgets/navigation_rail.dart';
import '../../widgets/mxc_image_viewer.dart';
import 'settings.dart'; import 'settings.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
@ -65,6 +66,7 @@ class SettingsView extends StatelessWidget {
future: controller.profileFuture, future: controller.profileFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
final profile = snapshot.data; final profile = snapshot.data;
final avatar = profile?.avatarUrl;
final mxid = Matrix.of(context).client.userID ?? final mxid = Matrix.of(context).client.userID ??
L10n.of(context).user; L10n.of(context).user;
final displayname = final displayname =
@ -76,9 +78,16 @@ class SettingsView extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
Avatar( Avatar(
mxContent: profile?.avatarUrl, mxContent: avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(avatar),
)
: null,
), ),
if (profile != null) if (profile != null)
Positioned( Positioned(

@ -169,6 +169,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Federation Base URL'), title: const Text('Federation Base URL'),
subtitle: Linkify( subtitle: Linkify(
text: data.federationBaseUrl.toString(), text: data.federationBaseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -231,6 +233,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Base URL'), title: const Text('Base URL'),
subtitle: Linkify( subtitle: Linkify(
text: wellKnown.mHomeserver.baseUrl.toString(), text: wellKnown.mHomeserver.baseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -244,6 +248,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Identity Server:'), title: const Text('Identity Server:'),
subtitle: Linkify( subtitle: Linkify(
text: identityServer.baseUrl.toString(), text: identityServer.baseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,

@ -16,6 +16,7 @@ import 'settings_security.dart';
class SettingsSecurityView extends StatelessWidget { class SettingsSecurityView extends StatelessWidget {
final SettingsSecurityController controller; final SettingsSecurityController controller;
const SettingsSecurityView(this.controller, {super.key}); const SettingsSecurityView(this.controller, {super.key});
@override @override
@ -143,7 +144,7 @@ class SettingsSecurityView extends StatelessWidget {
leading: const Icon(Icons.vpn_key_outlined), leading: const Icon(Icons.vpn_key_outlined),
subtitle: SelectableText( subtitle: SelectableText(
Matrix.of(context).client.fingerprintKey.beautified, Matrix.of(context).client.fingerprintKey.beautified,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
), ),
), ),
if (capabilities?.mChangePassword?.enabled != false || if (capabilities?.mChangePassword?.enabled != false ||

@ -12,21 +12,41 @@ Future<T?> showAdaptiveBottomSheet<T>({
bool isScrollControlled = true, bool isScrollControlled = true,
bool useRootNavigator = true, bool useRootNavigator = true,
}) { }) {
final maxHeight = min(MediaQuery.of(context).size.height - 32, 600); if (FluffyThemes.isColumnMode(context)) {
final dialogMode = FluffyThemes.isColumnMode(context); return showDialog<T>(
return showModalBottomSheet( context: context,
useRootNavigator: useRootNavigator,
barrierDismissible: isDismissible,
useSafeArea: true,
builder: (context) => Center(
child: Container(
margin: const EdgeInsets.all(16),
constraints: const BoxConstraints(
maxWidth: 480,
maxHeight: 720,
),
child: Material(
elevation: Theme.of(context).dialogTheme.elevation ?? 4,
shadowColor: Theme.of(context).dialogTheme.shadowColor,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: Theme.of(context).scaffoldBackgroundColor,
clipBehavior: Clip.hardEdge,
child: builder(context),
),
),
),
);
}
return showModalBottomSheet<T>(
context: context, context: context,
builder: (context) => Padding( builder: (context) => Padding(
padding: dialogMode padding: EdgeInsets.zero,
? const EdgeInsets.symmetric(vertical: 32.0)
: EdgeInsets.zero,
child: ClipRRect( child: ClipRRect(
borderRadius: dialogMode borderRadius: const BorderRadius.only(
? BorderRadius.circular(AppConfig.borderRadius) topLeft: Radius.circular(AppConfig.borderRadius / 2),
: const BorderRadius.only( topRight: Radius.circular(AppConfig.borderRadius / 2),
topLeft: Radius.circular(AppConfig.borderRadius), ),
topRight: Radius.circular(AppConfig.borderRadius),
),
child: builder(context), child: builder(context),
), ),
), ),
@ -34,7 +54,7 @@ Future<T?> showAdaptiveBottomSheet<T>({
isDismissible: isDismissible, isDismissible: isDismissible,
isScrollControlled: isScrollControlled, isScrollControlled: isScrollControlled,
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: maxHeight + (dialogMode ? 64 : 0), maxHeight: min(MediaQuery.of(context).size.height - 32, 600),
maxWidth: FluffyThemes.columnWidth * 1.25, maxWidth: FluffyThemes.columnWidth * 1.25,
), ),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,

@ -9,7 +9,7 @@ extension StringColor on String {
number += codeUnitAt(i); number += codeUnitAt(i);
} }
number = (number % 12) * 25.5; number = (number % 12) * 25.5;
return HSLColor.fromAHSL(1, number, 1, light).toColor(); return HSLColor.fromAHSL(0.75, number, 1, light).toColor();
} }
Color get color { Color get color {
@ -29,6 +29,6 @@ extension StringColor on String {
Color get lightColorAvatar { Color get lightColorAvatar {
_colorCache[this] ??= {}; _colorCache[this] ??= {};
return _colorCache[this]![0.4] ??= _getColorLight(0.4); return _colorCache[this]![0.45] ??= _getColorLight(0.45);
} }
} }

@ -8,12 +8,11 @@ import 'package:punycode/punycode.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import '../widgets/adaptive_dialogs/public_room_dialog.dart';
import 'platform_infos.dart'; import 'platform_infos.dart';
class UrlLauncher { class UrlLauncher {
@ -179,11 +178,10 @@ class UrlLauncher {
} }
return; return;
} else { } else {
await showAdaptiveBottomSheet( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => PublicRoomBottomSheet( builder: (c) => PublicRoomDialog(
roomAlias: identityParts.primaryIdentifier, roomAlias: identityParts.primaryIdentifier,
outerContext: context,
), ),
); );
} }

@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import '../../config/themes.dart';
import '../../utils/url_launcher.dart';
import '../avatar.dart';
import '../future_loading_dialog.dart';
import '../hover_builder.dart';
import '../matrix.dart';
import '../mxc_image_viewer.dart';
import 'adaptive_dialog_action.dart';
class PublicRoomDialog extends StatelessWidget {
final String? roomAlias;
final PublicRoomsChunk? chunk;
final List<String>? via;
const PublicRoomDialog({super.key, this.roomAlias, this.chunk, this.via});
void _joinRoom(BuildContext context) async {
final client = Matrix.of(context).client;
final chunk = this.chunk;
final knock = chunk?.joinRule == 'knock';
final result = await showFutureLoadingDialog<String>(
context: context,
future: () async {
if (chunk != null && client.getRoomById(chunk.roomId) != null) {
return chunk.roomId;
}
final roomId = chunk != null && knock
? await client.knockRoom(chunk.roomId, via: via)
: await client.joinRoom(
roomAlias ?? chunk!.roomId,
via: via,
);
if (!knock && client.getRoomById(roomId) == null) {
await client.waitForRoomInSync(roomId);
}
return roomId;
},
);
final roomId = result.result;
if (roomId == null) return;
if (knock && client.getRoomById(roomId) == null) {
Navigator.of(context).pop<bool>(true);
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).pleaseWaitUntilInvited,
);
return;
}
if (result.error != null) return;
if (!context.mounted) return;
Navigator.of(context).pop<bool>(true);
// don't open the room if the joined room is a space
if (chunk?.roomType != 'm.space' &&
!client.getRoomById(result.result!)!.isSpace) {
context.go('/rooms/$roomId');
}
return;
}
bool _testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
Future<PublicRoomsChunk> _search(BuildContext context) async {
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(context).client.queryPublicRooms(
server: roomAlias!.domain,
filter: PublicRoomQueryFilter(
genericSearchTerm: roomAlias,
),
);
if (!query.chunk.any(_testRoom)) {
throw (L10n.of(context).noRoomsFound);
}
return query.chunk.firstWhere(_testRoom);
}
@override
Widget build(BuildContext context) {
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
final roomLink = roomAlias ?? chunk?.roomId;
var copied = false;
return AlertDialog.adaptive(
title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256),
child: Text(
chunk?.name ?? roomAlias?.localpart ?? chunk?.roomId ?? 'Unknown',
textAlign: TextAlign.center,
),
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
child: FutureBuilder<PublicRoomsChunk>(
future: _search(context),
builder: (context, snapshot) {
final theme = Theme.of(context);
final profile = snapshot.data;
final avatar = profile?.avatarUrl;
final topic = profile?.topic;
return SingleChildScrollView(
child: Column(
spacing: 8,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (roomLink != null)
HoverBuilder(
builder: (context, hovered) => StatefulBuilder(
builder: (context, setState) => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: roomLink),
);
setState(() {
copied = true;
});
},
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Padding(
padding:
const EdgeInsets.only(right: 4.0),
child: AnimatedScale(
duration:
FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered
? 1.33
: copied
? 1.25
: 1.0,
child: Icon(
copied
? Icons.check_circle
: Icons.copy,
size: 12,
color: copied ? Colors.green : null,
),
),
),
),
TextSpan(text: roomLink),
],
style: theme.textTheme.bodyMedium
?.copyWith(fontSize: 10),
),
textAlign: TextAlign.center,
),
),
),
),
),
Center(
child: Avatar(
mxContent: avatar,
name: profile?.name ?? roomLink,
size: Avatar.defaultSize * 2,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) => MxcImageViewer(avatar),
)
: null,
),
),
if (profile?.numJoinedMembers != null)
Text(
L10n.of(context).countParticipants(
profile?.numJoinedMembers ?? 0,
),
style: const TextStyle(fontSize: 10),
textAlign: TextAlign.center,
),
if (topic != null && topic.isNotEmpty)
SelectableLinkify(
text: topic,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
textAlign: TextAlign.center,
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: theme.colorScheme.primary,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
],
),
);
},
),
),
actions: [
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => _joinRoom(context),
child: Text(
chunk?.joinRule == 'knock' &&
Matrix.of(context).client.getRoomById(chunk!.roomId) == null
? L10n.of(context).knock
: chunk?.roomType == 'm.space'
? L10n.of(context).joinSpace
: L10n.of(context).joinRoom,
),
),
AdaptiveDialogAction(
bigButtons: true,
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context).close),
),
],
);
}
}

@ -31,6 +31,7 @@ Future<OkCancelResult?> showOkCancelAlertDialog({
? null ? null
: SelectableLinkify( : SelectableLinkify(
text: message, text: message,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
linkStyle: TextStyle( linkStyle: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary, decorationColor: Theme.of(context).colorScheme.primary,
@ -81,6 +82,7 @@ Future<OkCancelResult?> showOkAlertDialog({
? null ? null
: SelectableLinkify( : SelectableLinkify(
text: message, text: message,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
linkStyle: TextStyle( linkStyle: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary, decorationColor: Theme.of(context).colorScheme.primary,

@ -49,6 +49,7 @@ Future<String?> showTextInputDialog({
if (message != null) if (message != null)
SelectableLinkify( SelectableLinkify(
text: message, text: message,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
linkStyle: TextStyle( linkStyle: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary, decorationColor: Theme.of(context).colorScheme.primary,

@ -15,6 +15,7 @@ import '../../utils/url_launcher.dart';
import '../future_loading_dialog.dart'; import '../future_loading_dialog.dart';
import '../hover_builder.dart'; import '../hover_builder.dart';
import '../matrix.dart'; import '../matrix.dart';
import '../mxc_image_viewer.dart';
class UserDialog extends StatelessWidget { class UserDialog extends StatelessWidget {
static Future<void> show({ static Future<void> show({
@ -45,6 +46,7 @@ class UserDialog extends StatelessWidget {
L10n.of(context).user; L10n.of(context).user;
var copied = false; var copied = false;
final theme = Theme.of(context); final theme = Theme.of(context);
final avatar = profile.avatarUrl;
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: ConstrainedBox( title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256), constraints: const BoxConstraints(maxWidth: 256),
@ -52,30 +54,31 @@ class UserDialog extends StatelessWidget {
), ),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
child: SelectionArea( child: PresenceBuilder(
child: PresenceBuilder( userId: profile.userId,
userId: profile.userId, client: Matrix.of(context).client,
client: Matrix.of(context).client, builder: (context, presence) {
builder: (context, presence) { if (presence == null) return const SizedBox.shrink();
if (presence == null) return const SizedBox.shrink(); final statusMsg = presence.statusMsg;
final statusMsg = presence.statusMsg; final lastActiveTimestamp = presence.lastActiveTimestamp;
final lastActiveTimestamp = presence.lastActiveTimestamp; final presenceText = presence.currentlyActive == true
final presenceText = presence.currentlyActive == true ? L10n.of(context).currentlyActive
? L10n.of(context).currentlyActive : lastActiveTimestamp != null
: lastActiveTimestamp != null ? L10n.of(context).lastActiveAgo(
? L10n.of(context).lastActiveAgo( lastActiveTimestamp.localizedTimeShort(context),
lastActiveTimestamp.localizedTimeShort(context), )
) : null;
: null; return SingleChildScrollView(
return SingleChildScrollView( child: Column(
child: Column( spacing: 8,
spacing: 8, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ HoverBuilder(
HoverBuilder( builder: (context, hovered) => StatefulBuilder(
builder: (context, hovered) => StatefulBuilder( builder: (context, setState) => MouseRegion(
builder: (context, setState) => GestureDetector( cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () { onTap: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: profile.userId), ClipboardData(text: profile.userId),
@ -118,37 +121,45 @@ class UserDialog extends StatelessWidget {
), ),
), ),
), ),
Center( ),
child: Avatar( Center(
mxContent: profile.avatarUrl, child: Avatar(
name: displayname, mxContent: avatar,
size: Avatar.defaultSize * 2, name: displayname,
), size: Avatar.defaultSize * 2,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) => MxcImageViewer(avatar),
)
: null,
), ),
if (presenceText != null) ),
Text( if (presenceText != null)
presenceText, Text(
style: const TextStyle(fontSize: 10), presenceText,
textAlign: TextAlign.center, style: const TextStyle(fontSize: 10),
), textAlign: TextAlign.center,
if (statusMsg != null) ),
Linkify( if (statusMsg != null)
text: statusMsg, SelectableLinkify(
textAlign: TextAlign.center, text: statusMsg,
options: const LinkifyOptions(humanize: false), textScaleFactor:
linkStyle: TextStyle( MediaQuery.textScalerOf(context).scale(1),
color: theme.colorScheme.primary, textAlign: TextAlign.center,
decoration: TextDecoration.underline, options: const LinkifyOptions(humanize: false),
decorationColor: theme.colorScheme.primary, linkStyle: TextStyle(
), color: theme.colorScheme.primary,
onOpen: (url) => decoration: TextDecoration.underline,
UrlLauncher(context, url.url).launchUrl(), decorationColor: theme.colorScheme.primary,
), ),
], onOpen: (url) =>
), UrlLauncher(context, url.url).launchUrl(),
); ),
}, ],
), ),
);
},
), ),
), ),
actions: [ actions: [

@ -62,18 +62,13 @@ class Avatar extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: noPic child: noPic
? Container( ? Container(
decoration: BoxDecoration( decoration: BoxDecoration(color: name?.lightColorAvatar),
gradient: LinearGradient(
colors: [name!.lightColorAvatar, name.color],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
fallbackLetters, fallbackLetters,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: 'RobotoMono',
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: (size / 2.5).roundToDouble(), fontSize: (size / 2.5).roundToDouble(),
@ -143,10 +138,12 @@ class Avatar extends StatelessWidget {
], ],
); );
if (onTap == null) return container; if (onTap == null) return container;
return InkWell( return MouseRegion(
onTap: onTap, cursor: SystemMouseCursors.click,
borderRadius: borderRadius, child: GestureDetector(
child: container, onTap: onTap,
child: container,
),
); );
} }
} }

@ -84,6 +84,17 @@ void showMemberActionsPopupMenu({
], ],
), ),
), ),
if (user.membership == Membership.knock)
PopupMenuItem(
value: _MemberActions.approve,
child: Row(
children: [
const Icon(Icons.how_to_reg_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).approve),
],
),
),
PopupMenuItem( PopupMenuItem(
enabled: user.room.canChangePowerLevel && user.canChangeUserPowerLevel, enabled: user.room.canChangePowerLevel && user.canChangeUserPowerLevel,
value: _MemberActions.setRole, value: _MemberActions.setRole,
@ -202,9 +213,14 @@ void showMemberActionsPopupMenu({
future: () => user.setPower(power), future: () => user.setPower(power),
); );
return; return;
case _MemberActions.approve:
await showFutureLoadingDialog(
context: context,
future: () => user.room.invite(user.id),
);
return;
case _MemberActions.kick: case _MemberActions.kick:
if (await showOkCancelAlertDialog( if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context, context: context,
title: L10n.of(context).areYouSure, title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes, okLabel: L10n.of(context).yes,
@ -220,7 +236,6 @@ void showMemberActionsPopupMenu({
return; return;
case _MemberActions.ban: case _MemberActions.ban:
if (await showOkCancelAlertDialog( if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context, context: context,
title: L10n.of(context).areYouSure, title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes, okLabel: L10n.of(context).yes,
@ -268,7 +283,6 @@ void showMemberActionsPopupMenu({
return; return;
case _MemberActions.unban: case _MemberActions.unban:
if (await showOkCancelAlertDialog( if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context, context: context,
title: L10n.of(context).areYouSure, title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes, okLabel: L10n.of(context).yes,
@ -290,6 +304,7 @@ enum _MemberActions {
setRole, setRole,
kick, kick,
ban, ban,
approve,
unban, unban,
report, report,
} }

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'mxc_image.dart';
class MxcImageViewer extends StatelessWidget {
final Uri mxContent;
const MxcImageViewer(this.mxContent, {super.key});
@override
Widget build(BuildContext context) {
final iconButtonStyle = IconButton.styleFrom(
backgroundColor: Colors.black.withAlpha(200),
foregroundColor: Colors.white,
);
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Scaffold(
backgroundColor: Colors.black.withAlpha(128),
extendBodyBehindAppBar: true,
appBar: AppBar(
elevation: 0,
leading: IconButton(
style: iconButtonStyle,
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
color: Colors.white,
tooltip: L10n.of(context).close,
),
backgroundColor: Colors.transparent,
),
body: InteractiveViewer(
minScale: 1.0,
maxScale: 10.0,
onInteractionEnd: (endDetails) {
if (endDetails.velocity.pixelsPerSecond.dy >
MediaQuery.of(context).size.height * 1.5) {
Navigator.of(context, rootNavigator: false).pop();
}
},
child: Center(
child: GestureDetector(
// Ignore taps to not go back here:
onTap: () {},
child: MxcImage(
key: ValueKey(mxContent.toString()),
uri: mxContent,
fit: BoxFit.contain,
isThumbnail: false,
animated: true,
),
),
),
),
),
);
}
}

@ -17,25 +17,24 @@ Future<int?> showPermissionChooser(
builder: (context) => AlertDialog.adaptive( builder: (context) => AlertDialog.adaptive(
title: Text(L10n.of(context).chatPermissions), title: Text(L10n.of(context).chatPermissions),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256), constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
child: Center( child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12.0, spacing: 12.0,
children: [ children: [
Text(L10n.of(context).setPermissionsLevelDescription), Text(L10n.of(context).setPermissionsLevelDescription),
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: error, valueListenable: error,
builder: (context, errorText, _) => DialogTextField( builder: (context, errorText, _) => DialogTextField(
controller: controller, controller: controller,
hintText: currentLevel.toString(), hintText: currentLevel.toString(),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
labelText: L10n.of(context).custom, labelText: L10n.of(context).custom,
errorText: errorText, errorText: errorText,
),
), ),
], ),
), ],
), ),
), ),
actions: [ actions: [

@ -1,233 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/qr_code_viewer.dart';
class PublicRoomBottomSheet extends StatelessWidget {
final String? roomAlias;
final BuildContext outerContext;
final PublicRoomsChunk? chunk;
final List<String>? via;
PublicRoomBottomSheet({
this.roomAlias,
required this.outerContext,
this.chunk,
this.via,
super.key,
}) {
assert(roomAlias != null || chunk != null);
}
void _joinRoom(BuildContext context) async {
final client = Matrix.of(outerContext).client;
final chunk = this.chunk;
final knock = chunk?.joinRule == 'knock';
final result = await showFutureLoadingDialog<String>(
context: context,
future: () async {
if (chunk != null && client.getRoomById(chunk.roomId) != null) {
return chunk.roomId;
}
final roomId = chunk != null && knock
? await client.knockRoom(chunk.roomId, via: via)
: await client.joinRoom(
roomAlias ?? chunk!.roomId,
via: via,
);
if (!knock && client.getRoomById(roomId) == null) {
await client.waitForRoomInSync(roomId);
}
return roomId;
},
);
if (knock) {
return;
}
if (result.error == null) {
Navigator.of(context).pop<bool>(true);
// don't open the room if the joined room is a space
if (chunk?.roomType != 'm.space' &&
!client.getRoomById(result.result!)!.isSpace) {
outerContext.go('/rooms/${result.result!}');
}
return;
}
}
bool _testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
Future<PublicRoomsChunk> _search() async {
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(outerContext).client.queryPublicRooms(
server: roomAlias!.domain,
filter: PublicRoomQueryFilter(
genericSearchTerm: roomAlias,
),
);
if (!query.chunk.any(_testRoom)) {
throw (L10n.of(outerContext).noRoomsFound);
}
return query.chunk.firstWhere(_testRoom);
}
@override
Widget build(BuildContext context) {
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
final roomLink = roomAlias ?? chunk?.roomId;
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
chunk?.name ?? roomAlias ?? chunk?.roomId ?? 'Unknown',
overflow: TextOverflow.fade,
),
leading: Center(
child: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
),
actions: roomAlias == null
? null
: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: IconButton(
icon: const Icon(Icons.qr_code_rounded),
onPressed: () => showQrCodeViewer(
context,
roomAlias,
),
),
),
],
),
body: FutureBuilder<PublicRoomsChunk>(
future: _search(),
builder: (context, snapshot) {
final theme = Theme.of(context);
final profile = snapshot.data;
return ListView(
padding: EdgeInsets.zero,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: profile == null
? const Center(
child: CircularProgressIndicator.adaptive(),
)
: Avatar(
client: Matrix.of(outerContext).client,
mxContent: profile.avatarUrl,
name: profile.name ?? roomAlias,
size: Avatar.defaultSize * 3,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: roomLink != null
? () => FluffyShare.share(
roomLink,
context,
copyOnly: true,
)
: null,
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
iconColor: theme.colorScheme.onSurface,
),
label: Text(
roomLink ?? '...',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(
Icons.groups_3_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
iconColor: theme.colorScheme.onSurface,
),
label: Text(
L10n.of(context).countParticipants(
profile?.numJoinedMembers ?? 0,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton.icon(
onPressed: () => _joinRoom(context),
label: Text(
chunk?.joinRule == 'knock' &&
Matrix.of(outerContext)
.client
.getRoomById(chunk!.roomId) ==
null
? L10n.of(context).knock
: chunk?.roomType == 'm.space'
? L10n.of(context).joinSpace
: L10n.of(context).joinRoom,
),
icon: const Icon(Icons.navigate_next),
),
),
const SizedBox(height: 16),
if (profile?.topic?.isNotEmpty ?? false)
ListTile(
subtitle: SelectableLinkify(
text: profile!.topic!,
linkStyle: TextStyle(
color: theme.colorScheme.primary,
decorationColor: theme.colorScheme.primary,
),
style: TextStyle(
fontSize: 14,
color: theme.textTheme.bodyMedium!.color,
),
options: const LinkifyOptions(humanize: false),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
),
],
);
},
),
),
);
}
}

@ -90,10 +90,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.12.0"
audio_session: audio_session:
dependency: transitive dependency: transitive
description: description:
@ -138,10 +138,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: boolean_selector name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
canonical_json: canonical_json:
dependency: transitive dependency: transitive
description: description:
@ -154,10 +154,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -194,18 +194,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.0" version: "1.19.1"
colorize: colorize:
dependency: transitive dependency: transitive
description: description:
@ -354,10 +354,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.2"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -370,10 +370,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.1"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1047,18 +1047,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.7" version: "10.0.8"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.8" version: "3.0.9"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -1135,10 +1135,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -1159,10 +1159,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
mgrs_dart: mgrs_dart:
dependency: transitive dependency: transitive
description: description:
@ -1271,10 +1271,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1383,10 +1383,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.6"
platform_detect: platform_detect:
dependency: transitive dependency: transitive
description: description:
@ -1463,10 +1463,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: process name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.2" version: "5.0.3"
proj4dart: proj4dart:
dependency: transitive dependency: transitive
description: description:
@ -1780,10 +1780,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.1"
sprintf: sprintf:
dependency: transitive dependency: transitive
description: description:
@ -1836,26 +1836,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.12.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.1"
string_validator: string_validator:
dependency: transitive dependency: transitive
description: description:
@ -1900,34 +1900,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
test: test:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.25.8" version: "1.25.15"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.4"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.5" version: "0.6.8"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -2164,10 +2164,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.0" version: "14.3.1"
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2297,5 +2297,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

@ -2,7 +2,7 @@ name: fluffychat
description: Chat with your friends. description: Chat with your friends.
publish_to: none publish_to: none
# On version bump also increase the build number for F-Droid # On version bump also increase the build number for F-Droid
version: 1.25.0+3537 version: 1.26.0+3538
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

@ -3,21 +3,21 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
desktop_drop desktop_drop
dynamic_color dynamic_color
emoji_picker_flutter emoji_picker_flutter
file_selector_windows file_selector_windows
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_webrtc flutter_webrtc
geolocator_windows geolocator_windows
pasteboard pasteboard
permission_handler_windows permission_handler_windows
record_windows record_windows
share_plus share_plus
sqlcipher_flutter_libs sqlcipher_flutter_libs
url_launcher_windows url_launcher_windows
window_to_front window_to_front
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
@ -25,14 +25,14 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
foreach (plugin ${FLUTTER_PLUGIN_LIST}) foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach (plugin) endforeach(plugin)
foreach (ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach (ffi_plugin) endforeach(ffi_plugin)

Loading…
Cancel
Save