Merge branch 'main' into fix-jagged-avatar-edges
This commit is contained in:
commit
c5b61aba64
47 changed files with 1352 additions and 836 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
|
||||
|
|
|
|||
376
CHANGELOG.md
376
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
# Privacy
|
||||
|
||||
FluffyChat is available on Android, iOS and as a web version. Desktop versions for Windows, Linux and macOS may follow.
|
||||
FluffyChat is available on Android, iOS, Linux and as a web version. Desktop versions for Windows and macOS may follow.
|
||||
|
||||
* [Matrix](#matrix)
|
||||
* [Database](#database)
|
||||
|
|
@ -109,4 +109,4 @@ To enhance user safety and help protect against the sexual abuse and exploitatio
|
|||
|
||||
In addition to reporting messages, users can also report other users following a similar process.
|
||||
|
||||
We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/
|
||||
We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/
|
||||
|
|
|
|||
|
|
@ -3211,5 +3211,8 @@
|
|||
"recordAVideo": "Record a video",
|
||||
"optionalMessage": "(Optional) message...",
|
||||
"notSupportedOnThisDevice": "Not supported on this device",
|
||||
"enterNewChat": "Enter new chat"
|
||||
"enterNewChat": "Enter new chat",
|
||||
"approve": "Approve",
|
||||
"youHaveKnocked": "You have knocked",
|
||||
"pleaseWaitUntilInvited": "Please wait now, until someone from the room invites you."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ abstract class FluffyThemes {
|
|||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
showCheckmark: false,
|
||||
backgroundColor: colorScheme.surfaceContainer,
|
||||
side: BorderSide.none,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
toolbarHeight: isColumnMode ? 72 : 56,
|
||||
shadowColor:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import '../key_verification/key_verification_dialog.dart';
|
|||
class BootstrapDialog extends StatefulWidget {
|
||||
final bool wipe;
|
||||
final Client client;
|
||||
|
||||
const BootstrapDialog({
|
||||
super.key,
|
||||
this.wipe = false,
|
||||
|
|
@ -132,7 +133,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
minLines: 2,
|
||||
maxLines: 4,
|
||||
readOnly: true,
|
||||
style: const TextStyle(fontFamily: 'UbuntuMono'),
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
controller: TextEditingController(text: key),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
|
|
@ -257,7 +258,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
? null
|
||||
: [AutofillHints.password],
|
||||
controller: _recoveryKeyTextEditingController,
|
||||
style: const TextStyle(fontFamily: 'UbuntuMono'),
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
hintStyle: TextStyle(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class ChatAppBarListTile extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Linkify(
|
||||
text: title,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ extension EventInfoDialogExtension on Event {
|
|||
class EventInfoDialog extends StatelessWidget {
|
||||
final Event event;
|
||||
final L10n l10n;
|
||||
|
||||
const EventInfoDialog({
|
||||
required this.event,
|
||||
required this.l10n,
|
||||
|
|
@ -41,10 +42,8 @@ class EventInfoDialog extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).messageInfo),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_downward_outlined),
|
||||
leading: CloseButton(
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
|
|
|
|||
|
|
@ -395,20 +395,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
if (fileDescription != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Linkify(
|
||||
text: fileDescription,
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: widget.fontSize,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: widget.linkColor,
|
||||
fontSize: widget.fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: widget.linkColor,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: widget.fontSize,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: widget.linkColor,
|
||||
fontSize: widget.fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: widget.linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -420,6 +427,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
/// To use a MatrixFile as an AudioSource for the just_audio package
|
||||
class MatrixFileAudioSource extends StreamAudioSource {
|
||||
final MatrixFile file;
|
||||
|
||||
MatrixFileAudioSource(this.file);
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
border: Border(
|
||||
left: BorderSide(
|
||||
color: textColor,
|
||||
width: 3,
|
||||
width: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -295,7 +295,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
),
|
||||
textStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontFamily: 'UbuntuMono',
|
||||
fontFamily: 'RobotoMono',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -79,12 +79,19 @@ class ImageBubble extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final borderRadius =
|
||||
var borderRadius =
|
||||
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
|
||||
|
||||
final fileDescription = event.fileDescription;
|
||||
final textColor = this.textColor;
|
||||
|
||||
if (fileDescription != null) {
|
||||
borderRadius = borderRadius.copyWith(
|
||||
bottomLeft: Radius.zero,
|
||||
bottomRight: Radius.zero,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
|
|
@ -122,20 +129,29 @@ class ImageBubble extends StatelessWidget {
|
|||
if (fileDescription != null && textColor != null)
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import 'message_content.dart';
|
|||
import 'message_reactions.dart';
|
||||
import 'reply_content.dart';
|
||||
import 'state_message.dart';
|
||||
import 'verification_request_content.dart';
|
||||
|
||||
class Message extends StatelessWidget {
|
||||
final Event event;
|
||||
|
|
@ -85,7 +84,7 @@ class Message extends StatelessWidget {
|
|||
|
||||
if (event.type == EventTypes.Message &&
|
||||
event.messageType == EventTypes.KeyVerificationRequest) {
|
||||
return VerificationRequestContent(event: event, timeline: timeline);
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
||||
final client = Matrix.of(context).client;
|
||||
|
|
@ -149,10 +148,6 @@ class Message extends StatelessWidget {
|
|||
event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 3);
|
||||
final noPadding = {
|
||||
MessageTypes.File,
|
||||
MessageTypes.Audio,
|
||||
}.contains(event.messageType);
|
||||
|
||||
if (ownMessage) {
|
||||
color =
|
||||
|
|
@ -338,12 +333,6 @@ class Message extends StatelessWidget {
|
|||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth:
|
||||
FluffyThemes.columnWidth * 1.5,
|
||||
|
|
@ -386,22 +375,32 @@ class Message extends StatelessWidget {
|
|||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 8,
|
||||
),
|
||||
child: InkWell(
|
||||
child: Material(
|
||||
color:
|
||||
Colors.transparent,
|
||||
borderRadius:
|
||||
ReplyContent
|
||||
.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage:
|
||||
ownMessage,
|
||||
timeline: timeline,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
ReplyContent
|
||||
.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage:
|
||||
ownMessage,
|
||||
timeline:
|
||||
timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -185,19 +185,26 @@ class MessageContent extends StatelessWidget {
|
|||
if (event.messageType == MessageTypes.Emote) {
|
||||
html = '* $html';
|
||||
}
|
||||
return HtmlMessage(
|
||||
html: html,
|
||||
textColor: textColor,
|
||||
room: event.room,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: HtmlMessage(
|
||||
html: html,
|
||||
textColor: textColor,
|
||||
room: event.room,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
);
|
||||
}
|
||||
// else we fall through to the normal message rendering
|
||||
|
|
@ -276,24 +283,32 @@ class MessageContent extends StatelessWidget {
|
|||
final bigEmotes = event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 3;
|
||||
return Linkify(
|
||||
text: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
hideReply: true,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 5 : fontSize,
|
||||
decoration: event.redacted ? TextDecoration.lineThrough : null,
|
||||
child: Linkify(
|
||||
text: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
hideReply: true,
|
||||
),
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 5 : fontSize,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize: fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
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:
|
||||
|
|
@ -350,13 +365,19 @@ class _ButtonContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
child: Text(
|
||||
'$icon $label',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: fontSize,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Text(
|
||||
'$icon $label',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageDownloadContent extends StatelessWidget {
|
||||
final Event event;
|
||||
|
|
@ -30,83 +28,79 @@ class MessageDownloadContent extends StatelessWidget {
|
|||
?.tryGet<String>('mimetype')
|
||||
?.toUpperCase() ??
|
||||
'UNKNOWN');
|
||||
final sizeString = event.sizeString;
|
||||
final sizeString = event.sizeString ?? '?MB';
|
||||
final fileDescription = event.fileDescription;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => event.saveFile(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_download_outlined,
|
||||
color: textColor,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
onTap: () => event.saveFile(context),
|
||||
child: Container(
|
||||
width: 400,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: textColor.withAlpha(32),
|
||||
child: Icon(Icons.file_download_outlined, color: textColor),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
filename,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
filetype,
|
||||
style: TextStyle(
|
||||
color: linkColor,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (sizeString != null)
|
||||
Text(
|
||||
sizeString,
|
||||
style: TextStyle(
|
||||
color: linkColor,
|
||||
),
|
||||
'$sizeString | $filetype',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: textColor, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (fileDescription != null)
|
||||
Linkify(
|
||||
text: fileDescription,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
if (fileDescription != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ class ReplyContent extends StatelessWidget {
|
|||
final Event replyEvent;
|
||||
final bool ownMessage;
|
||||
final Timeline? timeline;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const ReplyContent(
|
||||
this.replyEvent, {
|
||||
this.ownMessage = false,
|
||||
super.key,
|
||||
this.timeline,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
static const BorderRadius borderRadius = BorderRadius.only(
|
||||
|
|
@ -40,16 +38,18 @@ class ReplyContent extends StatelessWidget {
|
|||
: theme.colorScheme.tertiary;
|
||||
|
||||
return Material(
|
||||
color: backgroundColor ??
|
||||
theme.colorScheme.surface.withAlpha(ownMessage ? 50 : 80),
|
||||
color: Colors.transparent,
|
||||
borderRadius: borderRadius,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 3,
|
||||
width: 5,
|
||||
height: fontSize * 2 + 16,
|
||||
color: color,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class VerificationRequestContent extends StatelessWidget {
|
||||
final Event event;
|
||||
final Timeline timeline;
|
||||
|
||||
const VerificationRequestContent({
|
||||
required this.event,
|
||||
required this.timeline,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final events = event.aggregatedEvents(timeline, 'm.reference');
|
||||
final done = events.where((e) => e.type == EventTypes.KeyVerificationDone);
|
||||
final start =
|
||||
events.where((e) => e.type == EventTypes.KeyVerificationStart);
|
||||
final cancel =
|
||||
events.where((e) => e.type == EventTypes.KeyVerificationCancel);
|
||||
final fullyDone = done.length >= 2;
|
||||
final started = start.isNotEmpty;
|
||||
final canceled = cancel.isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.lock_outlined,
|
||||
color: canceled
|
||||
? Colors.red
|
||||
: (fullyDone ? Colors.green : Colors.grey),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
canceled
|
||||
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
|
||||
: (fullyDone
|
||||
? L10n.of(context).verifySuccess
|
||||
: (started
|
||||
? L10n.of(context).loadingPleaseWait
|
||||
: L10n.of(context).newVerificationRequest)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ class EventVideoPlayer extends StatefulWidget {
|
|||
final Event event;
|
||||
final Color? textColor;
|
||||
final Color? linkColor;
|
||||
|
||||
const EventVideoPlayer(
|
||||
this.event, {
|
||||
this.textColor,
|
||||
|
|
@ -188,20 +189,29 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
if (fileDescription != null && textColor != null && linkColor != null)
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
child: Linkify(
|
||||
text: fileDescription,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: linkColor,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: linkColor,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ class InputBar extends StatelessWidget {
|
|||
children: [
|
||||
Text(
|
||||
commandExample(command),
|
||||
style: const TextStyle(fontFamily: 'UbuntuMono'),
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
),
|
||||
Text(
|
||||
hint,
|
||||
|
|
@ -255,7 +255,7 @@ class InputBar extends StatelessWidget {
|
|||
waitDuration: const Duration(days: 1), // don't show on hover
|
||||
child: Container(
|
||||
padding: padding,
|
||||
child: Text(label, style: const TextStyle(fontFamily: 'UbuntuMono')),
|
||||
child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class ReplyDisplay extends StatelessWidget {
|
|||
? ReplyContent(
|
||||
controller.replyEvent!,
|
||||
timeline: controller.timeline!,
|
||||
backgroundColor: Colors.transparent,
|
||||
)
|
||||
: _EditContent(
|
||||
controller.editEvent?.getDisplayEvent(controller.timeline!),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
|||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
import '../../widgets/mxc_image_viewer.dart';
|
||||
import '../../widgets/qr_code_viewer.dart';
|
||||
|
||||
class ChatDetailsView extends StatelessWidget {
|
||||
|
|
@ -38,6 +39,7 @@ class ChatDetailsView extends StatelessWidget {
|
|||
}
|
||||
|
||||
final directChatMatrixID = room.directChatMatrixID;
|
||||
final roomAvatar = room.avatar;
|
||||
|
||||
return StreamBuilder(
|
||||
stream: room.client.onRoomState.stream
|
||||
|
|
@ -108,6 +110,13 @@ class ChatDetailsView extends StatelessWidget {
|
|||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
onTap: roomAvatar != null
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
MxcImageViewer(roomAvatar),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat &&
|
||||
|
|
@ -234,6 +243,8 @@ class ChatDetailsView extends StatelessWidget {
|
|||
text: room.topic.isEmpty
|
||||
? L10n.of(context).noChatDescriptionYet
|
||||
: room.topic,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.blueAccent,
|
||||
|
|
|
|||
|
|
@ -30,65 +30,68 @@ class ParticipantListItem extends StatelessWidget {
|
|||
? L10n.of(context).moderator
|
||||
: '';
|
||||
|
||||
return Opacity(
|
||||
opacity: user.membership == Membership.join ? 1 : 0.5,
|
||||
child: ListTile(
|
||||
onTap: () => showMemberActionsPopupMenu(context: context, user: user),
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
return ListTile(
|
||||
onTap: () => showMemberActionsPopupMenu(context: context, user: user),
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
user.calcDisplayname(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (permissionBatch.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.tertiary
|
||||
: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
user.calcDisplayname(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
permissionBatch,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.onTertiary
|
||||
: theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
membershipBatch == null
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
permissionBatch,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: user.powerLevel >= 100
|
||||
? 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,
|
||||
child: Center(
|
||||
child: Text(
|
||||
membershipBatch,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
user.id,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leading: Avatar(
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
user.id,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leading: Opacity(
|
||||
opacity: user.membership == Membership.join ? 1 : 0.5,
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
presenceUserId: user.stateKey,
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
|||
deviceKeys[i].ed25519Key?.beautified ??
|
||||
L10n.of(context).unknownEncryptionAlgorithm,
|
||||
style: TextStyle(
|
||||
fontFamily: 'UbuntuMono',
|
||||
fontFamily: 'RobotoMono',
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,11 +11,9 @@ import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
|
|||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import '../../config/themes.dart';
|
||||
import '../../widgets/adaptive_dialogs/user_dialog.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
|
@ -155,7 +153,7 @@ class ChatListViewBody extends StatelessWidget {
|
|||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 16.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
|
@ -172,53 +170,15 @@ class ChatListViewBody extends StatelessWidget {
|
|||
]
|
||||
.map(
|
||||
(filter) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) =>
|
||||
AnimatedScale(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
scale: hovered ? 1.1 : 1.0,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
onTap: () =>
|
||||
controller.setActiveFilter(filter),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: filter ==
|
||||
controller.activeFilter
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme
|
||||
.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
filter.toLocalizedString(context),
|
||||
style: TextStyle(
|
||||
fontWeight: filter ==
|
||||
controller.activeFilter
|
||||
? FontWeight.w500
|
||||
: FontWeight.normal,
|
||||
color: filter ==
|
||||
controller.activeFilter
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: FilterChip(
|
||||
selected: filter == controller.activeFilter,
|
||||
onSelected: (_) =>
|
||||
controller.setActiveFilter(filter),
|
||||
label:
|
||||
Text(filter.toLocalizedString(context)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -341,12 +301,11 @@ class PublicRoomsHorizontalList extends StatelessWidget {
|
|||
publicRooms[i].canonicalAlias?.localpart ??
|
||||
L10n.of(context).group,
|
||||
avatar: publicRooms[i].avatarUrl,
|
||||
onPressed: () => showAdaptiveBottomSheet(
|
||||
onPressed: () => showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (c) => PublicRoomBottomSheet(
|
||||
builder: (c) => PublicRoomDialog(
|
||||
roomAlias:
|
||||
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
|
||||
outerContext: context,
|
||||
chunk: publicRooms[i],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,16 +10,15 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
|
||||
enum AddRoomType { chat, subspace }
|
||||
|
||||
|
|
@ -100,10 +99,9 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final client = Matrix.of(context).client;
|
||||
final space = client.getRoomById(widget.spaceId);
|
||||
|
||||
final joined = await showAdaptiveBottomSheet<bool>(
|
||||
final joined = await showAdaptiveDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => PublicRoomBottomSheet(
|
||||
outerContext: context,
|
||||
builder: (_) => PublicRoomDialog(
|
||||
chunk: item,
|
||||
via: space?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -7,6 +9,7 @@ import 'chat_members_view.dart';
|
|||
|
||||
class ChatMembersPage extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
const ChatMembersPage({required this.roomId, super.key});
|
||||
|
||||
@override
|
||||
|
|
@ -17,12 +20,27 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
List<User>? members;
|
||||
List<User>? filteredMembers;
|
||||
Object? error;
|
||||
Membership membershipFilter = Membership.join;
|
||||
|
||||
final TextEditingController filterController = TextEditingController();
|
||||
|
||||
void setMembershipFilter(Membership membership) {
|
||||
membershipFilter = membership;
|
||||
setFilter();
|
||||
}
|
||||
|
||||
void setFilter([_]) async {
|
||||
final filter = filterController.text.toLowerCase().trim();
|
||||
|
||||
final members = this
|
||||
.members
|
||||
?.where(
|
||||
(member) =>
|
||||
membershipFilter == Membership.join ||
|
||||
member.membership == membershipFilter,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (filter.isEmpty) {
|
||||
setState(() {
|
||||
filteredMembers = members
|
||||
|
|
@ -42,7 +60,8 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
});
|
||||
}
|
||||
|
||||
void refreshMembers() async {
|
||||
void refreshMembers([_]) async {
|
||||
Logs().d('Load room members from', widget.roomId);
|
||||
try {
|
||||
setState(() {
|
||||
error = null;
|
||||
|
|
@ -50,7 +69,9 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
final participants = await Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
?.requestParticipants();
|
||||
?.requestParticipants(
|
||||
[...Membership.values]..remove(Membership.leave),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
|
|
@ -67,10 +88,30 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
}
|
||||
}
|
||||
|
||||
StreamSubscription? _updateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
refreshMembers();
|
||||
|
||||
_updateSub = Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.rooms?.join?[widget.roomId]?.timeline?.events
|
||||
?.any((state) => state.type == EventTypes.RoomMember) ??
|
||||
false,
|
||||
)
|
||||
.listen(refreshMembers);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_updateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import '../../widgets/layouts/max_width_body.dart';
|
||||
|
|
@ -11,6 +12,7 @@ import 'chat_members.dart';
|
|||
|
||||
class ChatMembersView extends StatelessWidget {
|
||||
final ChatMembersController controller;
|
||||
|
||||
const ChatMembersView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
|
|
@ -84,29 +86,89 @@ class ChatMembersView extends StatelessWidget {
|
|||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: members.length + 1,
|
||||
itemBuilder: (context, i) => i == 0
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: controller.filterController,
|
||||
onChanged: controller.setFilter,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
final availableFilters = Membership.values
|
||||
.where(
|
||||
(membership) =>
|
||||
controller.members?.any(
|
||||
(member) => member.membership == membership,
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.toList();
|
||||
availableFilters
|
||||
.sort((a, b) => a == Membership.join ? -1 : 1);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: controller.filterController,
|
||||
onChanged: controller.setFilter,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor:
|
||||
theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
hintText: L10n.of(context).search,
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
hintText: L10n.of(context).search,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ParticipantListItem(members[i - 1]),
|
||||
if (availableFilters.length > 1)
|
||||
SizedBox(
|
||||
height: 64,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: availableFilters.length,
|
||||
itemBuilder: (context, i) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: FilterChip(
|
||||
label: Text(
|
||||
switch (availableFilters[i]) {
|
||||
Membership.ban =>
|
||||
L10n.of(context).banned,
|
||||
Membership.invite =>
|
||||
L10n.of(context).invited,
|
||||
Membership.join =>
|
||||
L10n.of(context).all,
|
||||
Membership.knock =>
|
||||
L10n.of(context).knocking,
|
||||
Membership.leave =>
|
||||
L10n.of(context).leftTheChat,
|
||||
},
|
||||
),
|
||||
selected: controller.membershipFilter ==
|
||||
availableFilters[i],
|
||||
onSelected: (_) =>
|
||||
controller.setMembershipFilter(
|
||||
availableFilters[i],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
i--;
|
||||
return ParticipantListItem(members[i]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ class _MessageSearchResultListTile extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
subtitle: Linkify(
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ class HomeserverPickerView extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: SelectableLinkify(
|
||||
text: L10n.of(context).appIntroduction,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
textAlign: TextAlign.center,
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
|
|
@ -169,6 +171,19 @@ class HomeserverPickerView extends StatelessWidget {
|
|||
content: Linkify(
|
||||
text: L10n.of(context)
|
||||
.homeserverDescription,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context)
|
||||
.scale(1),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decorationColor:
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
onOpen: (link) =>
|
||||
launchUrlString(link.url),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
|
|||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
import '../../widgets/mxc_image_viewer.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
class SettingsView extends StatelessWidget {
|
||||
|
|
@ -65,6 +66,7 @@ class SettingsView extends StatelessWidget {
|
|||
future: controller.profileFuture,
|
||||
builder: (context, snapshot) {
|
||||
final profile = snapshot.data;
|
||||
final avatar = profile?.avatarUrl;
|
||||
final mxid = Matrix.of(context).client.userID ??
|
||||
L10n.of(context).user;
|
||||
final displayname =
|
||||
|
|
@ -76,9 +78,16 @@ class SettingsView extends StatelessWidget {
|
|||
child: Stack(
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: profile?.avatarUrl,
|
||||
mxContent: avatar,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
onTap: avatar != null
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
MxcImageViewer(avatar),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (profile != null)
|
||||
Positioned(
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ class SettingsHomeserverView extends StatelessWidget {
|
|||
title: const Text('Federation Base URL'),
|
||||
subtitle: Linkify(
|
||||
text: data.federationBaseUrl.toString(),
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
|
|
@ -231,6 +233,8 @@ class SettingsHomeserverView extends StatelessWidget {
|
|||
title: const Text('Base URL'),
|
||||
subtitle: Linkify(
|
||||
text: wellKnown.mHomeserver.baseUrl.toString(),
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
|
|
@ -244,6 +248,8 @@ class SettingsHomeserverView extends StatelessWidget {
|
|||
title: const Text('Identity Server:'),
|
||||
subtitle: Linkify(
|
||||
text: identityServer.baseUrl.toString(),
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'settings_security.dart';
|
|||
|
||||
class SettingsSecurityView extends StatelessWidget {
|
||||
final SettingsSecurityController controller;
|
||||
|
||||
const SettingsSecurityView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
|
|
@ -143,7 +144,7 @@ class SettingsSecurityView extends StatelessWidget {
|
|||
leading: const Icon(Icons.vpn_key_outlined),
|
||||
subtitle: SelectableText(
|
||||
Matrix.of(context).client.fingerprintKey.beautified,
|
||||
style: const TextStyle(fontFamily: 'UbuntuMono'),
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
),
|
||||
),
|
||||
if (capabilities?.mChangePassword?.enabled != false ||
|
||||
|
|
|
|||
|
|
@ -12,21 +12,41 @@ Future<T?> showAdaptiveBottomSheet<T>({
|
|||
bool isScrollControlled = true,
|
||||
bool useRootNavigator = true,
|
||||
}) {
|
||||
final maxHeight = min(MediaQuery.of(context).size.height - 32, 600);
|
||||
final dialogMode = FluffyThemes.isColumnMode(context);
|
||||
return showModalBottomSheet(
|
||||
if (FluffyThemes.isColumnMode(context)) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
useRootNavigator: useRootNavigator,
|
||||
barrierDismissible: isDismissible,
|
||||
useSafeArea: true,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 480,
|
||||
maxHeight: 720,
|
||||
),
|
||||
child: Material(
|
||||
elevation: Theme.of(context).dialogTheme.elevation ?? 4,
|
||||
shadowColor: Theme.of(context).dialogTheme.shadowColor,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: builder(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: dialogMode
|
||||
? const EdgeInsets.symmetric(vertical: 32.0)
|
||||
: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: dialogMode
|
||||
? BorderRadius.circular(AppConfig.borderRadius)
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius / 2),
|
||||
topRight: Radius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
child: builder(context),
|
||||
),
|
||||
),
|
||||
|
|
@ -34,7 +54,7 @@ Future<T?> showAdaptiveBottomSheet<T>({
|
|||
isDismissible: isDismissible,
|
||||
isScrollControlled: isScrollControlled,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight + (dialogMode ? 64 : 0),
|
||||
maxHeight: min(MediaQuery.of(context).size.height - 32, 600),
|
||||
maxWidth: FluffyThemes.columnWidth * 1.25,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ extension StringColor on String {
|
|||
number += codeUnitAt(i);
|
||||
}
|
||||
number = (number % 12) * 25.5;
|
||||
return HSLColor.fromAHSL(1, number, 1, light).toColor();
|
||||
return HSLColor.fromAHSL(0.75, number, 1, light).toColor();
|
||||
}
|
||||
|
||||
Color get color {
|
||||
|
|
@ -29,6 +29,6 @@ extension StringColor on String {
|
|||
|
||||
Color get lightColorAvatar {
|
||||
_colorCache[this] ??= {};
|
||||
return _colorCache[this]![0.4] ??= _getColorLight(0.4);
|
||||
return _colorCache[this]![0.45] ??= _getColorLight(0.45);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,11 @@ import 'package:punycode/punycode.dart';
|
|||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import '../widgets/adaptive_dialogs/public_room_dialog.dart';
|
||||
import 'platform_infos.dart';
|
||||
|
||||
class UrlLauncher {
|
||||
|
|
@ -179,11 +178,10 @@ class UrlLauncher {
|
|||
}
|
||||
return;
|
||||
} else {
|
||||
await showAdaptiveBottomSheet(
|
||||
await showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (c) => PublicRoomBottomSheet(
|
||||
builder: (c) => PublicRoomDialog(
|
||||
roomAlias: identityParts.primaryIdentifier,
|
||||
outerContext: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
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
|
||||
: SelectableLinkify(
|
||||
text: message,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decorationColor: Theme.of(context).colorScheme.primary,
|
||||
|
|
@ -81,6 +82,7 @@ Future<OkCancelResult?> showOkAlertDialog({
|
|||
? null
|
||||
: SelectableLinkify(
|
||||
text: message,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decorationColor: Theme.of(context).colorScheme.primary,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ Future<String?> showTextInputDialog({
|
|||
if (message != null)
|
||||
SelectableLinkify(
|
||||
text: message,
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decorationColor: Theme.of(context).colorScheme.primary,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import '../../utils/url_launcher.dart';
|
|||
import '../future_loading_dialog.dart';
|
||||
import '../hover_builder.dart';
|
||||
import '../matrix.dart';
|
||||
import '../mxc_image_viewer.dart';
|
||||
|
||||
class UserDialog extends StatelessWidget {
|
||||
static Future<void> show({
|
||||
|
|
@ -45,6 +46,7 @@ class UserDialog extends StatelessWidget {
|
|||
L10n.of(context).user;
|
||||
var copied = false;
|
||||
final theme = Theme.of(context);
|
||||
final avatar = profile.avatarUrl;
|
||||
return AlertDialog.adaptive(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
|
|
@ -52,30 +54,31 @@ class UserDialog extends StatelessWidget {
|
|||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: SelectionArea(
|
||||
child: PresenceBuilder(
|
||||
userId: profile.userId,
|
||||
client: Matrix.of(context).client,
|
||||
builder: (context, presence) {
|
||||
if (presence == null) return const SizedBox.shrink();
|
||||
final statusMsg = presence.statusMsg;
|
||||
final lastActiveTimestamp = presence.lastActiveTimestamp;
|
||||
final presenceText = presence.currentlyActive == true
|
||||
? L10n.of(context).currentlyActive
|
||||
: lastActiveTimestamp != null
|
||||
? L10n.of(context).lastActiveAgo(
|
||||
lastActiveTimestamp.localizedTimeShort(context),
|
||||
)
|
||||
: null;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
HoverBuilder(
|
||||
builder: (context, hovered) => StatefulBuilder(
|
||||
builder: (context, setState) => GestureDetector(
|
||||
child: PresenceBuilder(
|
||||
userId: profile.userId,
|
||||
client: Matrix.of(context).client,
|
||||
builder: (context, presence) {
|
||||
if (presence == null) return const SizedBox.shrink();
|
||||
final statusMsg = presence.statusMsg;
|
||||
final lastActiveTimestamp = presence.lastActiveTimestamp;
|
||||
final presenceText = presence.currentlyActive == true
|
||||
? L10n.of(context).currentlyActive
|
||||
: lastActiveTimestamp != null
|
||||
? L10n.of(context).lastActiveAgo(
|
||||
lastActiveTimestamp.localizedTimeShort(context),
|
||||
)
|
||||
: null;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
HoverBuilder(
|
||||
builder: (context, hovered) => StatefulBuilder(
|
||||
builder: (context, setState) => MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: profile.userId),
|
||||
|
|
@ -118,37 +121,45 @@ class UserDialog extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Avatar(
|
||||
mxContent: profile.avatarUrl,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Avatar(
|
||||
mxContent: avatar,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2,
|
||||
onTap: avatar != null
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => MxcImageViewer(avatar),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (presenceText != null)
|
||||
Text(
|
||||
presenceText,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (presenceText != null)
|
||||
Text(
|
||||
presenceText,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (statusMsg != null)
|
||||
SelectableLinkify(
|
||||
text: statusMsg,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(context).scale(1),
|
||||
textAlign: TextAlign.center,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: theme.colorScheme.primary,
|
||||
),
|
||||
if (statusMsg != null)
|
||||
Linkify(
|
||||
text: statusMsg,
|
||||
textAlign: TextAlign.center,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: theme.colorScheme.primary,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -62,18 +62,13 @@ class Avatar extends StatelessWidget {
|
|||
clipBehavior: Clip.antiAlias,
|
||||
child: noPic
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [name!.lightColorAvatar, name.color],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(color: name?.lightColorAvatar),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
fallbackLetters,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'RobotoMono',
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: (size / 2.5).roundToDouble(),
|
||||
|
|
@ -143,10 +138,12 @@ class Avatar extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
if (onTap == null) return container;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: borderRadius,
|
||||
child: container,
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,17 @@ void showMemberActionsPopupMenu({
|
|||
],
|
||||
),
|
||||
),
|
||||
if (user.membership == Membership.knock)
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.approve,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.how_to_reg_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).approve),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: user.room.canChangePowerLevel && user.canChangeUserPowerLevel,
|
||||
value: _MemberActions.setRole,
|
||||
|
|
@ -202,9 +213,14 @@ void showMemberActionsPopupMenu({
|
|||
future: () => user.setPower(power),
|
||||
);
|
||||
return;
|
||||
case _MemberActions.approve:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => user.room.invite(user.id),
|
||||
);
|
||||
return;
|
||||
case _MemberActions.kick:
|
||||
if (await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
|
|
@ -220,7 +236,6 @@ void showMemberActionsPopupMenu({
|
|||
return;
|
||||
case _MemberActions.ban:
|
||||
if (await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
|
|
@ -268,7 +283,6 @@ void showMemberActionsPopupMenu({
|
|||
return;
|
||||
case _MemberActions.unban:
|
||||
if (await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
|
|
@ -290,6 +304,7 @@ enum _MemberActions {
|
|||
setRole,
|
||||
kick,
|
||||
ban,
|
||||
approve,
|
||||
unban,
|
||||
report,
|
||||
}
|
||||
|
|
|
|||
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(
|
||||
title: Text(L10n.of(context).chatPermissions),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(L10n.of(context).setPermissionsLevelDescription),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: error,
|
||||
builder: (context, errorText, _) => DialogTextField(
|
||||
controller: controller,
|
||||
hintText: currentLevel.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
labelText: L10n.of(context).custom,
|
||||
errorText: errorText,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(L10n.of(context).setPermissionsLevelDescription),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: error,
|
||||
builder: (context, errorText, _) => DialogTextField(
|
||||
controller: controller,
|
||||
hintText: currentLevel.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
labelText: L10n.of(context).custom,
|
||||
errorText: errorText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/qr_code_viewer.dart';
|
||||
|
||||
class PublicRoomBottomSheet extends StatelessWidget {
|
||||
final String? roomAlias;
|
||||
final BuildContext outerContext;
|
||||
final PublicRoomsChunk? chunk;
|
||||
final List<String>? via;
|
||||
|
||||
PublicRoomBottomSheet({
|
||||
this.roomAlias,
|
||||
required this.outerContext,
|
||||
this.chunk,
|
||||
this.via,
|
||||
super.key,
|
||||
}) {
|
||||
assert(roomAlias != null || chunk != null);
|
||||
}
|
||||
|
||||
void _joinRoom(BuildContext context) async {
|
||||
final client = Matrix.of(outerContext).client;
|
||||
final chunk = this.chunk;
|
||||
final knock = chunk?.joinRule == 'knock';
|
||||
final result = await showFutureLoadingDialog<String>(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (chunk != null && client.getRoomById(chunk.roomId) != null) {
|
||||
return chunk.roomId;
|
||||
}
|
||||
final roomId = chunk != null && knock
|
||||
? await client.knockRoom(chunk.roomId, via: via)
|
||||
: await client.joinRoom(
|
||||
roomAlias ?? chunk!.roomId,
|
||||
via: via,
|
||||
);
|
||||
|
||||
if (!knock && client.getRoomById(roomId) == null) {
|
||||
await client.waitForRoomInSync(roomId);
|
||||
}
|
||||
return roomId;
|
||||
},
|
||||
);
|
||||
if (knock) {
|
||||
return;
|
||||
}
|
||||
if (result.error == null) {
|
||||
Navigator.of(context).pop<bool>(true);
|
||||
// don't open the room if the joined room is a space
|
||||
if (chunk?.roomType != 'm.space' &&
|
||||
!client.getRoomById(result.result!)!.isSpace) {
|
||||
outerContext.go('/rooms/${result.result!}');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool _testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
|
||||
|
||||
Future<PublicRoomsChunk> _search() async {
|
||||
final chunk = this.chunk;
|
||||
if (chunk != null) return chunk;
|
||||
final query = await Matrix.of(outerContext).client.queryPublicRooms(
|
||||
server: roomAlias!.domain,
|
||||
filter: PublicRoomQueryFilter(
|
||||
genericSearchTerm: roomAlias,
|
||||
),
|
||||
);
|
||||
if (!query.chunk.any(_testRoom)) {
|
||||
throw (L10n.of(outerContext).noRoomsFound);
|
||||
}
|
||||
return query.chunk.firstWhere(_testRoom);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
|
||||
final roomLink = roomAlias ?? chunk?.roomId;
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
chunk?.name ?? roomAlias ?? chunk?.roomId ?? 'Unknown',
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
leading: Center(
|
||||
child: CloseButton(
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
),
|
||||
),
|
||||
actions: roomAlias == null
|
||||
? null
|
||||
: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.qr_code_rounded),
|
||||
onPressed: () => showQrCodeViewer(
|
||||
context,
|
||||
roomAlias,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<PublicRoomsChunk>(
|
||||
future: _search(),
|
||||
builder: (context, snapshot) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final profile = snapshot.data;
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: profile == null
|
||||
? const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
: Avatar(
|
||||
client: Matrix.of(outerContext).client,
|
||||
mxContent: profile.avatarUrl,
|
||||
name: profile.name ?? roomAlias,
|
||||
size: Avatar.defaultSize * 3,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: roomLink != null
|
||||
? () => FluffyShare.share(
|
||||
roomLink,
|
||||
context,
|
||||
copyOnly: true,
|
||||
)
|
||||
: null,
|
||||
icon: const Icon(
|
||||
Icons.copy_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
iconColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
roomLink ?? '...',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.groups_3_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
iconColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
profile?.numJoinedMembers ?? 0,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _joinRoom(context),
|
||||
label: Text(
|
||||
chunk?.joinRule == 'knock' &&
|
||||
Matrix.of(outerContext)
|
||||
.client
|
||||
.getRoomById(chunk!.roomId) ==
|
||||
null
|
||||
? L10n.of(context).knock
|
||||
: chunk?.roomType == 'm.space'
|
||||
? L10n.of(context).joinSpace
|
||||
: L10n.of(context).joinRoom,
|
||||
),
|
||||
icon: const Icon(Icons.navigate_next),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (profile?.topic?.isNotEmpty ?? false)
|
||||
ListTile(
|
||||
subtitle: SelectableLinkify(
|
||||
text: profile!.topic!,
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decorationColor: theme.colorScheme.primary,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.textTheme.bodyMedium!.color,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
pubspec.lock
94
pubspec.lock
|
|
@ -90,10 +90,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
audio_session:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -138,10 +138,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
canonical_json:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -154,10 +154,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -194,18 +194,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.19.1"
|
||||
colorize:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -354,10 +354,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -370,10 +370,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1047,18 +1047,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1135,10 +1135,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1159,10 +1159,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1271,10 +1271,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1383,10 +1383,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
version: "3.1.6"
|
||||
platform_detect:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1463,10 +1463,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.0.3"
|
||||
proj4dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1780,10 +1780,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1836,26 +1836,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.1"
|
||||
string_validator:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1900,34 +1900,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.8"
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
version: "0.6.8"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2164,10 +2164,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.3.1"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -2297,5 +2297,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: fluffychat
|
|||
description: Chat with your friends.
|
||||
publish_to: none
|
||||
# On version bump also increase the build number for F-Droid
|
||||
version: 1.25.0+3537
|
||||
version: 1.26.0+3538
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
desktop_drop
|
||||
dynamic_color
|
||||
emoji_picker_flutter
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
flutter_webrtc
|
||||
geolocator_windows
|
||||
pasteboard
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
share_plus
|
||||
sqlcipher_flutter_libs
|
||||
url_launcher_windows
|
||||
window_to_front
|
||||
app_links
|
||||
desktop_drop
|
||||
dynamic_color
|
||||
emoji_picker_flutter
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
flutter_webrtc
|
||||
geolocator_windows
|
||||
pasteboard
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
share_plus
|
||||
sqlcipher_flutter_libs
|
||||
url_launcher_windows
|
||||
window_to_front
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
@ -25,14 +25,14 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
|||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach (plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach (plugin)
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach (ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach (ffi_plugin)
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue