Compare commits

...

5 Commits

Author SHA1 Message Date
josé m 85d3a11030
Translated using Weblate (Galician)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/
2023-01-16 07:47:59 +01:00
Krille bf084f1ccc chore: Remove unused dependency 2023-01-15 10:06:02 +01:00
Krille 4afb9a4790 chore: Follow up leave abandoned DM room 2023-01-15 10:05:54 +01:00
TheOneWithTheBraid ddb7cc841b chore: add integration tests for spaces
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-01-14 10:46:18 +01:00
Krille a1f60b7ff9 fix: Archive 2023-01-13 10:38:39 +00:00
15 changed files with 182 additions and 66 deletions

View File

@ -2342,7 +2342,7 @@
"user": {}
}
},
"youInvitedUser": "📩 Convidaches a {user}",
"youInvitedUser": "📩 Convidaches a {user}",
"@youInvitedUser": {
"placeholders": {
"user": {}
@ -2537,5 +2537,7 @@
"placeholders": {
"oldDisplayName": {}
}
}
},
"reopenChat": "Reabrir conversa",
"@reopenChat": {}
}

View File

@ -2,6 +2,7 @@ import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@ -116,6 +117,72 @@ void main() {
await tester.pumpAndSettle();
},
);
testWidgets('Spaces', (tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.waitFor(find.byTooltip('Show menu'));
await tester.tap(find.byTooltip('Show menu'));
await tester.pumpAndSettle();
await tester.waitFor(find.byIcon(Icons.workspaces_outlined));
await tester.tap(find.byIcon(Icons.workspaces_outlined));
await tester.pumpAndSettle();
await tester.waitFor(find.byType(TextField));
await tester.enterText(find.byType(TextField).last, 'Test Space');
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.waitFor(find.text('Invite contact'));
await tester.tap(find.text('Invite contact'));
await tester.pumpAndSettle();
await tester.waitFor(
find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)),
);
await tester.enterText(
find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)),
Users.user2.name,
);
await Future.delayed(const Duration(milliseconds: 250));
await tester.testTextInput.receiveAction(TextInputAction.done);
await Future.delayed(const Duration(milliseconds: 1000));
await tester.pumpAndSettle();
await tester.tap(find
.descendant(
of: find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(ListTile),
),
matching: find.text(Users.user2.name))
.last);
await tester.pumpAndSettle();
await tester.waitFor(find.maybeUppercaseText('Yes'));
await tester.tap(find.maybeUppercaseText('Yes'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
await tester.waitFor(find.text('Load 2 more participants'));
await tester.tap(find.text('Load 2 more participants'));
await tester.pumpAndSettle();
expect(find.text(Users.user2.name), findsOneWidget);
});
},
);
}

View File

@ -94,6 +94,13 @@ class AppRoutes {
VWidget(
path: '/archive',
widget: const Archive(),
stackedRoutes: [
VWidget(
path: ':roomid',
widget: const Chat(),
buildTransition: _dynamicTransition,
),
],
),
VWidget(
path: '/newprivatechat',
@ -220,13 +227,25 @@ class AppRoutes {
),
],
),
VWidget(
VNester(
path: '/archive',
widget: const TwoColumnLayout(
mainView: Archive(),
sideView: EmptyPage(),
widgetBuilder: (child) => TwoColumnLayout(
mainView: const Archive(),
sideView: child,
),
buildTransition: _fadeTransition,
nestedRoutes: [
VWidget(
path: '',
widget: const EmptyPage(),
buildTransition: _dynamicTransition,
),
VWidget(
path: ':roomid',
widget: const Chat(),
buildTransition: _dynamicTransition,
),
],
),
],
),

View File

@ -21,11 +21,9 @@ class ArchiveController extends State<Archive> {
Future<List<Room>> getArchive(BuildContext context) async {
final archive = this.archive;
if (archive != null) return archive;
return await Matrix.of(context).client.loadArchive();
return this.archive = await Matrix.of(context).client.loadArchive();
}
void forgetAction(int i) => setState(() => archive?.removeAt(i));
void forgetAllAction() async {
final archive = this.archive;
if (archive == null) return;

View File

@ -21,10 +21,14 @@ class ArchiveView extends StatelessWidget {
leading: const BackButton(),
title: Text(L10n.of(context)!.archive),
actions: [
if (snapshot.hasData && archive != null && archive!.isNotEmpty)
TextButton(
onPressed: controller.forgetAllAction,
child: Text(L10n.of(context)!.clearArchive),
if (snapshot.data?.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
onPressed: controller.forgetAllAction,
label: Text(L10n.of(context)!.clearArchive),
icon: const Icon(Icons.cleaning_services_outlined),
),
)
],
),
@ -50,7 +54,6 @@ class ArchiveView extends StatelessWidget {
itemCount: archive!.length,
itemBuilder: (BuildContext context, int i) => ChatListItem(
archive![i],
onForget: controller.forgetAction,
),
);
}

View File

@ -165,6 +165,20 @@ class ChatController extends State<Chat> {
VRouter.of(context).toSegments(['rooms', roomId]);
}
void leaveChat() async {
final room = this.room;
if (room == null) {
throw Exception(
'Leave room button clicked while room is null. This should not be possible from the UI!');
}
final success = await showFutureLoadingDialog(
context: context,
future: room.leave,
);
if (success.error != null) return;
VRouter.of(context).to('/rooms');
}
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
void requestHistory() async {
@ -630,6 +644,7 @@ class ChatController extends State<Chat> {
}
bool get canRedactSelectedEvents {
if (isArchived) return false;
final clients = matrix!.currentBundle;
for (final event in selectedEvents) {
if (event.canRedact == false &&
@ -639,7 +654,9 @@ class ChatController extends State<Chat> {
}
bool get canEditSelectedEvents {
if (selectedEvents.length != 1 || !selectedEvents.first.status.isSent) {
if (isArchived ||
selectedEvents.length != 1 ||
!selectedEvents.first.status.isSent) {
return false;
}
return currentRoomBundle
@ -757,6 +774,15 @@ class ChatController extends State<Chat> {
return sendEmojiAction(emoji.emoji);
}
void forgetRoom() async {
final result = await showFutureLoadingDialog(
context: context,
future: room!.forget,
);
if (result.error != null) return;
VRouter.of(context).to('/archive');
}
void typeEmoji(Emoji? emoji) {
if (emoji == null) return;
final text = sendController.text;
@ -1008,6 +1034,9 @@ class ChatController extends State<Chat> {
setState(() => inputText = text);
}
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room?.membership);
void showEventInfo([Event? event]) =>
(event ?? selectedEvents.single).showInfoDialog(context);

View File

@ -37,7 +37,10 @@ class ChatAppBarTitle extends StatelessWidget {
'${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ',
),
)
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),
: controller.isArchived
? null
: () =>
VRouter.of(context).toSegments(['rooms', room.id, 'details']),
child: Row(
children: [
Hero(

View File

@ -108,6 +108,20 @@ class ChatView extends StatelessWidget {
],
),
];
} else if (controller.isArchived) {
return [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
onPressed: controller.forgetRoom,
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
icon: const Icon(Icons.delete_forever_outlined),
label: Text(L10n.of(context)!.delete),
),
)
];
} else {
return [
if (Matrix.of(context).voipPlugin != null &&
@ -284,6 +298,8 @@ class ChatView extends StatelessWidget {
children: [
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(16),
foregroundColor:
Theme.of(context)
.colorScheme
@ -292,12 +308,16 @@ class ChatView extends StatelessWidget {
icon: const Icon(
Icons.archive_outlined,
),
onPressed: () {},
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(16),
),
icon: const Icon(
Icons.chat_outlined,
),

View File

@ -21,10 +21,11 @@ class PinnedEvents extends StatelessWidget {
BuildContext context, List<Event?> events) async {
final eventId = events.length == 1
? events.single?.eventId
: await showModalActionSheet<String>(
: await showConfirmationDialog<String>(
context: context,
title: L10n.of(context)!.pinMessage,
actions: events
.map((event) => SheetAction(
.map((event) => AlertDialogAction(
key: event?.eventId ?? '',
label: event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),

View File

@ -21,7 +21,6 @@ class ChatListItem extends StatelessWidget {
final Room room;
final bool activeChat;
final bool selected;
final Function? onForget;
final void Function()? onTap;
final void Function()? onLongPress;
@ -31,7 +30,6 @@ class ChatListItem extends StatelessWidget {
this.selected = false,
this.onTap,
this.onLongPress,
this.onForget,
Key? key,
}) : super(key: key);
@ -62,35 +60,7 @@ class ChatListItem extends StatelessWidget {
}
if (room.membership == Membership.leave) {
final action = await showModalActionSheet<ArchivedRoomAction>(
context: context,
title: L10n.of(context)!.archivedRoom,
message: L10n.of(context)!.thisRoomHasBeenArchived,
actions: [
SheetAction(
label: L10n.of(context)!.rejoin,
key: ArchivedRoomAction.rejoin,
),
SheetAction(
label: L10n.of(context)!.delete,
key: ArchivedRoomAction.delete,
isDestructiveAction: true,
),
],
);
if (action != null) {
switch (action) {
case ArchivedRoomAction.delete:
await archiveAction(context);
break;
case ArchivedRoomAction.rejoin:
await showFutureLoadingDialog(
context: context,
future: () => room.join(),
);
break;
}
}
VRouter.of(context).toSegments(['archive', room.id]);
}
if (room.membership == Membership.join) {
@ -122,13 +92,10 @@ class ChatListItem extends StatelessWidget {
Future<void> archiveAction(BuildContext context) async {
{
if ([Membership.leave, Membership.ban].contains(room.membership)) {
final success = await showFutureLoadingDialog(
await showFutureLoadingDialog(
context: context,
future: () => room.forget(),
);
if (success.error == null) {
if (onForget != null) onForget!();
}
return;
}
final confirmed = await showOkCancelAlertDialog(

View File

@ -205,12 +205,3 @@ class ChatListView extends StatelessWidget {
);
}
}
enum ChatListPopupMenuItemActions {
createGroup,
createSpace,
discover,
setStatus,
inviteContact,
settings,
}

View File

@ -66,6 +66,16 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
PopupMenuItem(
value: SettingsAction.archive,
child: Row(
children: [
const Icon(Icons.archive_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.archive),
],
),
),
PopupMenuItem(
value: SettingsAction.settings,
child: Row(
@ -259,6 +269,9 @@ class ClientChooserButton extends StatelessWidget {
case SettingsAction.settings:
VRouter.of(context).to('/settings');
break;
case SettingsAction.archive:
VRouter.of(context).to('/archive');
break;
}
}
}
@ -336,4 +349,5 @@ enum SettingsAction {
newSpace,
invite,
settings,
archive,
}

View File

@ -38,7 +38,7 @@ class NewSpaceController extends State<NewSpace> {
),
);
if (roomID.error == null) {
VRouter.of(context).toSegments(['rooms', roomID.result!, 'details']);
VRouter.of(context).toSegments(['spaces', roomID.result!]);
}
}

View File

@ -8,12 +8,14 @@ Future<T?> showAdaptiveBottomSheet<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,
bool isDismissible = true,
bool isScrollControlled = false,
}) =>
showModalBottomSheet(
context: context,
builder: builder,
useRootNavigator: !PlatformInfos.isMobile,
isDismissible: isDismissible,
isScrollControlled: isScrollControlled,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height - 128,
maxWidth: FluffyThemes.columnWidth * 1.5,

View File

@ -29,12 +29,12 @@ extension on PermissionLevel {
Future<int?> showPermissionChooser(BuildContext context,
{int currentLevel = 0}) async {
final permissionLevel = await showModalActionSheet(
final permissionLevel = await showConfirmationDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
actions: PermissionLevel.values
.map(
(level) => SheetAction(
(level) => AlertDialogAction(
key: level,
label: level.toLocalizedString(context),
),