Merge branch 'main' into fix-jagged-avatar-edges
This commit is contained in:
commit
c5b61aba64
47 changed files with 1351 additions and 835 deletions
26
.github/workflows/matrix_notification.yaml
vendored
Normal file
26
.github/workflows/matrix_notification.yaml
vendored
Normal file
|
|
@ -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
|
||||||
2
.github/workflows/versions.env
vendored
2
.github/workflows/versions.env
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
FLUTTER_VERSION=3.29.2
|
FLUTTER_VERSION=3.29.3
|
||||||
JAVA_VERSION=17
|
JAVA_VERSION=17
|
||||||
|
|
|
||||||
374
CHANGELOG.md
374
CHANGELOG.md
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(
|
child: Linkify(
|
||||||
color: textColor,
|
text: event.calcLocalizedBodyFallback(
|
||||||
fontSize: bigEmotes ? fontSize * 5 : fontSize,
|
MatrixLocals(L10n.of(context)),
|
||||||
decoration: event.redacted ? TextDecoration.lineThrough : null,
|
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(),
|
||||||
),
|
),
|
||||||
options: const LinkifyOptions(humanize: false),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
color: linkColor,
|
|
||||||
fontSize: fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
decorationColor: linkColor,
|
|
||||||
),
|
|
||||||
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(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
user.calcDisplayname(),
|
permissionBatch,
|
||||||
overflow: TextOverflow.ellipsis,
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: user.powerLevel >= 100
|
||||||
|
? theme.colorScheme.onTertiary
|
||||||
|
: theme.colorScheme.onTertiaryContainer,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (permissionBatch.isNotEmpty)
|
membershipBatch == null
|
||||||
Container(
|
? const SizedBox.shrink()
|
||||||
padding: const EdgeInsets.symmetric(
|
: Container(
|
||||||
horizontal: 12,
|
padding:
|
||||||
vertical: 6,
|
const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: user.powerLevel >= 100
|
color: theme.colorScheme.secondaryContainer,
|
||||||
? theme.colorScheme.tertiary
|
borderRadius: BorderRadius.circular(8),
|
||||||
: theme.colorScheme.tertiaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppConfig.borderRadius,
|
|
||||||
),
|
),
|
||||||
),
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
permissionBatch,
|
membershipBatch,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: user.powerLevel >= 100
|
color: theme.colorScheme.onSecondaryContainer,
|
||||||
? 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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
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);
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: theme.colorScheme.onPrimaryContainer,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
231
lib/widgets/adaptive_dialogs/public_room_dialog.dart
Normal file
231
lib/widgets/adaptive_dialogs/public_room_dialog.dart
Normal file
|
|
@ -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)
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
if (statusMsg != null)
|
onOpen: (url) =>
|
||||||
Linkify(
|
UrlLauncher(context, url.url).launchUrl(),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
lib/widgets/mxc_image_viewer.dart
Normal file
60
lib/widgets/mxc_image_viewer.dart
Normal file
|
|
@ -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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
94
pubspec.lock
94
pubspec.lock
|
|
@ -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…
Add table
Add a link
Reference in a new issue