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

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
# 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)
* [Database](#database)

@ -3211,5 +3211,8 @@
"recordAVideo": "Record a video",
"optionalMessage": "(Optional) message...",
"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),
),
chipTheme: ChipThemeData(
showCheckmark: false,
backgroundColor: colorScheme.surfaceContainer,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
appBarTheme: AppBarTheme(
toolbarHeight: isColumnMode ? 72 : 56,
shadowColor:

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

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

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

@ -395,20 +395,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
),
if (fileDescription != null) ...[
const SizedBox(height: 8),
Linkify(
text: fileDescription,
style: TextStyle(
color: widget.color,
fontSize: widget.fontSize,
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: widget.linkColor,
fontSize: widget.fontSize,
decoration: TextDecoration.underline,
decorationColor: widget.linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: widget.color,
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
class MatrixFileAudioSource extends StreamAudioSource {
final MatrixFile file;
MatrixFileAudioSource(this.file);
@override

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

@ -79,12 +79,19 @@ class ImageBubble extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final borderRadius =
var borderRadius =
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
final fileDescription = event.fileDescription;
final textColor = this.textColor;
if (fileDescription != null) {
borderRadius = borderRadius.copyWith(
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
@ -122,20 +129,29 @@ class ImageBubble extends StatelessWidget {
if (fileDescription != null && textColor != null)
SizedBox(
width: width,
child: Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
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 'reply_content.dart';
import 'state_message.dart';
import 'verification_request_content.dart';
class Message extends StatelessWidget {
final Event event;
@ -85,7 +84,7 @@ class Message extends StatelessWidget {
if (event.type == EventTypes.Message &&
event.messageType == EventTypes.KeyVerificationRequest) {
return VerificationRequestContent(event: event, timeline: timeline);
return StateMessage(event);
}
final client = Matrix.of(context).client;
@ -149,10 +148,6 @@ class Message extends StatelessWidget {
event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 3);
final noPadding = {
MessageTypes.File,
MessageTypes.Audio,
}.contains(event.messageType);
if (ownMessage) {
color =
@ -338,12 +333,6 @@ class Message extends StatelessWidget {
AppConfig.borderRadius,
),
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 1.5,
@ -386,22 +375,32 @@ class Message extends StatelessWidget {
return Padding(
padding:
const EdgeInsets.only(
bottom: 4.0,
left: 16,
right: 16,
top: 8,
),
child: InkWell(
child: Material(
color:
Colors.transparent,
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline: timeline,
child: InkWell(
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
),
),
),

@ -185,19 +185,26 @@ class MessageContent extends StatelessWidget {
if (event.messageType == MessageTypes.Emote) {
html = '* $html';
}
return HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
linkStyle: TextStyle(
color: linkColor,
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
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(),
);
}
// else we fall through to the normal message rendering
@ -276,24 +283,32 @@ class MessageContent extends StatelessWidget {
final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 3;
return Linkify(
text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
),
style: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 5 : fontSize,
decoration: event.redacted ? TextDecoration.lineThrough : null,
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
),
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:
@ -350,13 +365,19 @@ class _ButtonContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Text(
'$icon $label',
style: TextStyle(
color: textColor,
fontSize: fontSize,
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
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/utils/file_description.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.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 {
final Event event;
@ -30,83 +28,79 @@ class MessageDownloadContent extends StatelessWidget {
?.tryGet<String>('mimetype')
?.toUpperCase() ??
'UNKNOWN');
final sizeString = event.sizeString;
final sizeString = event.sizeString ?? '?MB';
final fileDescription = event.fileDescription;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
InkWell(
onTap: () => event.saveFile(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
Icons.file_download_outlined,
color: textColor,
),
const SizedBox(width: 16),
Flexible(
child: Text(
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
onTap: () => event.saveFile(context),
child: Container(
width: 400,
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
CircleAvatar(
backgroundColor: textColor.withAlpha(32),
child: Icon(Icons.file_download_outlined, color: textColor),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
filename,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
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(
sizeString,
style: TextStyle(
color: linkColor,
),
'$sizeString | $filetype',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: textColor, fontSize: 10),
),
],
),
],
),
],
),
],
),
),
),
if (fileDescription != null)
Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
if (fileDescription != null) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
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 bool ownMessage;
final Timeline? timeline;
final Color? backgroundColor;
const ReplyContent(
this.replyEvent, {
this.ownMessage = false,
super.key,
this.timeline,
this.backgroundColor,
});
static const BorderRadius borderRadius = BorderRadius.only(
@ -40,16 +38,18 @@ class ReplyContent extends StatelessWidget {
: theme.colorScheme.tertiary;
return Material(
color: backgroundColor ??
theme.colorScheme.surface.withAlpha(ownMessage ? 50 : 80),
color: Colors.transparent,
borderRadius: borderRadius,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 3,
width: 5,
height: fontSize * 2 + 16,
color: color,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: color,
),
),
const SizedBox(width: 6),
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 Color? textColor;
final Color? linkColor;
const EventVideoPlayer(
this.event, {
this.textColor,
@ -188,20 +189,29 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
if (fileDescription != null && textColor != null && linkColor != null)
SizedBox(
width: width,
child: Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
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: [
Text(
commandExample(command),
style: const TextStyle(fontFamily: 'UbuntuMono'),
style: const TextStyle(fontFamily: 'RobotoMono'),
),
Text(
hint,
@ -255,7 +255,7 @@ class InputBar extends StatelessWidget {
waitDuration: const Duration(days: 1), // don't show on hover
child: Container(
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(
controller.replyEvent!,
timeline: controller.timeline!,
backgroundColor: Colors.transparent,
)
: _EditContent(
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/matrix.dart';
import '../../utils/url_launcher.dart';
import '../../widgets/mxc_image_viewer.dart';
import '../../widgets/qr_code_viewer.dart';
class ChatDetailsView extends StatelessWidget {
@ -38,6 +39,7 @@ class ChatDetailsView extends StatelessWidget {
}
final directChatMatrixID = room.directChatMatrixID;
final roomAvatar = room.avatar;
return StreamBuilder(
stream: room.client.onRoomState.stream
@ -108,6 +110,13 @@ class ChatDetailsView extends StatelessWidget {
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
onTap: roomAvatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(roomAvatar),
)
: null,
),
),
if (!room.isDirectChat &&
@ -234,6 +243,8 @@ class ChatDetailsView extends StatelessWidget {
text: room.topic.isEmpty
? L10n.of(context).noChatDescriptionYet
: room.topic,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,

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

@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context).unknownEncryptionAlgorithm,
style: TextStyle(
fontFamily: 'UbuntuMono',
fontFamily: 'RobotoMono',
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/space_view.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/widgets/adaptive_dialogs/public_room_dialog.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 '../../widgets/adaptive_dialogs/user_dialog.dart';
import '../../widgets/matrix.dart';
@ -155,7 +153,7 @@ class ChatListViewBody extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 16.0,
vertical: 12.0,
),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
@ -172,53 +170,15 @@ class ChatListViewBody extends StatelessWidget {
]
.map(
(filter) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4),
child: HoverBuilder(
builder: (context, hovered) =>
AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: InkWell(
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,
),
),
),
),
),
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected: filter == controller.activeFilter,
onSelected: (_) =>
controller.setActiveFilter(filter),
label:
Text(filter.toLocalizedString(context)),
),
),
)
@ -341,12 +301,11 @@ class PublicRoomsHorizontalList extends StatelessWidget {
publicRooms[i].canonicalAlias?.localpart ??
L10n.of(context).group,
avatar: publicRooms[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
onPressed: () => showAdaptiveDialog(
context: context,
builder: (c) => PublicRoomBottomSheet(
builder: (c) => PublicRoomDialog(
roomAlias:
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
outerContext: context,
chunk: publicRooms[i],
),
),

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

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -7,6 +9,7 @@ import 'chat_members_view.dart';
class ChatMembersPage extends StatefulWidget {
final String roomId;
const ChatMembersPage({required this.roomId, super.key});
@override
@ -17,12 +20,27 @@ class ChatMembersController extends State<ChatMembersPage> {
List<User>? members;
List<User>? filteredMembers;
Object? error;
Membership membershipFilter = Membership.join;
final TextEditingController filterController = TextEditingController();
void setMembershipFilter(Membership membership) {
membershipFilter = membership;
setFilter();
}
void setFilter([_]) async {
final filter = filterController.text.toLowerCase().trim();
final members = this
.members
?.where(
(member) =>
membershipFilter == Membership.join ||
member.membership == membershipFilter,
)
.toList();
if (filter.isEmpty) {
setState(() {
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 {
setState(() {
error = null;
@ -50,7 +69,9 @@ class ChatMembersController extends State<ChatMembersPage> {
final participants = await Matrix.of(context)
.client
.getRoomById(widget.roomId)
?.requestParticipants();
?.requestParticipants(
[...Membership.values]..remove(Membership.leave),
);
if (!mounted) return;
@ -67,10 +88,30 @@ class ChatMembersController extends State<ChatMembersPage> {
}
}
StreamSubscription? _updateSub;
@override
void initState() {
super.initState();
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

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import '../../widgets/layouts/max_width_body.dart';
@ -11,6 +12,7 @@ import 'chat_members.dart';
class ChatMembersView extends StatelessWidget {
final ChatMembersController controller;
const ChatMembersView(this.controller, {super.key});
@override
@ -84,29 +86,89 @@ class ChatMembersView extends StatelessWidget {
: ListView.builder(
shrinkWrap: true,
itemCount: members.length + 1,
itemBuilder: (context, i) => i == 0
? 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,
itemBuilder: (context, i) {
if (i == 0) {
final availableFilters = Membership.values
.where(
(membership) =>
controller.members?.any(
(member) => member.membership == membership,
) ??
false,
)
.toList();
availableFilters
.sort((a, b) => a == Membership.join ? -1 : 1);
return Column(
mainAxisSize: MainAxisSize.min,
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,
),
),
)
: ParticipantListItem(members[i - 1]),
if (availableFilters.length > 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(
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,

@ -122,6 +122,8 @@ class HomeserverPickerView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: SelectableLinkify(
text: L10n.of(context).appIntroduction,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
textAlign: TextAlign.center,
linkStyle: TextStyle(
color: theme.colorScheme.secondary,
@ -169,6 +171,19 @@ class HomeserverPickerView extends StatelessWidget {
content: Linkify(
text: L10n.of(context)
.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: [
AdaptiveDialogAction(

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

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

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

@ -12,21 +12,41 @@ Future<T?> showAdaptiveBottomSheet<T>({
bool isScrollControlled = true,
bool useRootNavigator = true,
}) {
final maxHeight = min(MediaQuery.of(context).size.height - 32, 600);
final dialogMode = FluffyThemes.isColumnMode(context);
return showModalBottomSheet(
if (FluffyThemes.isColumnMode(context)) {
return showDialog<T>(
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,
builder: (context) => Padding(
padding: dialogMode
? const EdgeInsets.symmetric(vertical: 32.0)
: EdgeInsets.zero,
padding: EdgeInsets.zero,
child: ClipRRect(
borderRadius: dialogMode
? BorderRadius.circular(AppConfig.borderRadius)
: const BorderRadius.only(
topLeft: Radius.circular(AppConfig.borderRadius),
topRight: Radius.circular(AppConfig.borderRadius),
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(AppConfig.borderRadius / 2),
topRight: Radius.circular(AppConfig.borderRadius / 2),
),
child: builder(context),
),
),
@ -34,7 +54,7 @@ Future<T?> showAdaptiveBottomSheet<T>({
isDismissible: isDismissible,
isScrollControlled: isScrollControlled,
constraints: BoxConstraints(
maxHeight: maxHeight + (dialogMode ? 64 : 0),
maxHeight: min(MediaQuery.of(context).size.height - 32, 600),
maxWidth: FluffyThemes.columnWidth * 1.25,
),
backgroundColor: Colors.transparent,

@ -9,7 +9,7 @@ extension StringColor on String {
number += codeUnitAt(i);
}
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 {
@ -29,6 +29,6 @@ extension StringColor on String {
Color get lightColorAvatar {
_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: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/user_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.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';
class UrlLauncher {
@ -179,11 +178,10 @@ class UrlLauncher {
}
return;
} else {
await showAdaptiveBottomSheet(
await showAdaptiveDialog(
context: context,
builder: (c) => PublicRoomBottomSheet(
builder: (c) => PublicRoomDialog(
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
: SelectableLinkify(
text: message,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
linkStyle: TextStyle(
color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary,
@ -81,6 +82,7 @@ Future<OkCancelResult?> showOkAlertDialog({
? null
: SelectableLinkify(
text: message,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
linkStyle: TextStyle(
color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary,

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

@ -62,18 +62,13 @@ class Avatar extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: noPic
? Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [name!.lightColorAvatar, name.color],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(color: name?.lightColorAvatar),
alignment: Alignment.center,
child: Text(
fallbackLetters,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'RobotoMono',
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: (size / 2.5).roundToDouble(),
@ -143,10 +138,12 @@ class Avatar extends StatelessWidget {
],
);
if (onTap == null) return container;
return InkWell(
onTap: onTap,
borderRadius: borderRadius,
child: container,
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
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(
enabled: user.room.canChangePowerLevel && user.canChangeUserPowerLevel,
value: _MemberActions.setRole,
@ -202,9 +213,14 @@ void showMemberActionsPopupMenu({
future: () => user.setPower(power),
);
return;
case _MemberActions.approve:
await showFutureLoadingDialog(
context: context,
future: () => user.room.invite(user.id),
);
return;
case _MemberActions.kick:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
@ -220,7 +236,6 @@ void showMemberActionsPopupMenu({
return;
case _MemberActions.ban:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
@ -268,7 +283,6 @@ void showMemberActionsPopupMenu({
return;
case _MemberActions.unban:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
@ -290,6 +304,7 @@ enum _MemberActions {
setRole,
kick,
ban,
approve,
unban,
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(
title: Text(L10n.of(context).chatPermissions),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 12.0,
children: [
Text(L10n.of(context).setPermissionsLevelDescription),
ValueListenableBuilder(
valueListenable: error,
builder: (context, errorText, _) => DialogTextField(
controller: controller,
hintText: currentLevel.toString(),
keyboardType: TextInputType.number,
labelText: L10n.of(context).custom,
errorText: errorText,
),
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12.0,
children: [
Text(L10n.of(context).setPermissionsLevelDescription),
ValueListenableBuilder(
valueListenable: error,
builder: (context, errorText, _) => DialogTextField(
controller: controller,
hintText: currentLevel.toString(),
keyboardType: TextInputType.number,
labelText: L10n.of(context).custom,
errorText: errorText,
),
],
),
),
],
),
),
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"
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.12.0"
audio_session:
dependency: transitive
description:
@ -138,10 +138,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
canonical_json:
dependency: transitive
description:
@ -154,10 +154,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
charcode:
dependency: transitive
description:
@ -194,18 +194,18 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
collection:
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
colorize:
dependency: transitive
description:
@ -354,10 +354,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
ffi:
dependency: transitive
description:
@ -370,10 +370,10 @@ packages:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
@ -1047,18 +1047,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@ -1135,10 +1135,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@ -1159,10 +1159,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
mgrs_dart:
dependency: transitive
description:
@ -1271,10 +1271,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
@ -1383,10 +1383,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
version: "3.1.6"
platform_detect:
dependency: transitive
description:
@ -1463,10 +1463,10 @@ packages:
dependency: transitive
description:
name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "5.0.3"
proj4dart:
dependency: transitive
description:
@ -1780,10 +1780,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sprintf:
dependency: transitive
description:
@ -1836,26 +1836,26 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
string_validator:
dependency: transitive
description:
@ -1900,34 +1900,34 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.dev"
source: hosted
version: "1.25.8"
version: "1.25.15"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.8"
timezone:
dependency: transitive
description:
@ -2164,10 +2164,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
version: "14.3.1"
wakelock_plus:
dependency: "direct main"
description:
@ -2297,5 +2297,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.0"

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

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

Loading…
Cancel
Save