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)

@ -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