added talk notification levels for chats
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
|||||||
|
|
||||||
import '../../../api_params.dart';
|
import '../../../api_params.dart';
|
||||||
import '../../../api_response.dart';
|
import '../../../api_response.dart';
|
||||||
|
import '../room/get_room_response.dart';
|
||||||
import '../talk_api.dart';
|
import '../talk_api.dart';
|
||||||
|
|
||||||
/// Small POST/DELETE-only Talk endpoints that have no response payload.
|
/// Small POST/DELETE-only Talk endpoints that have no response payload.
|
||||||
@@ -105,3 +106,21 @@ class DeleteRoomAvatar extends TalkApi {
|
|||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
) => http.delete(uri, headers: headers);
|
) => http.delete(uri, headers: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SetNotificationLevel extends TalkApi {
|
||||||
|
final String chatToken;
|
||||||
|
final GetRoomResponseObjectParticipantNotificationLevel level;
|
||||||
|
|
||||||
|
SetNotificationLevel(this.chatToken, this.level)
|
||||||
|
: super('v4/room/$chatToken/notify', null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ApiResponse? assemble(String raw) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<http.Response> request(
|
||||||
|
Uri uri,
|
||||||
|
ApiParams? body,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
) => http.post(uri, headers: headers, body: {'level': '${level.apiValue}'});
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ enum GetRoomResponseObjectParticipantNotificationLevel {
|
|||||||
neverNotify,
|
neverNotify,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationLevelApiValue
|
||||||
|
on GetRoomResponseObjectParticipantNotificationLevel {
|
||||||
|
int get apiValue =>
|
||||||
|
_$GetRoomResponseObjectParticipantNotificationLevelEnumMap[this]!;
|
||||||
|
}
|
||||||
|
|
||||||
enum GetRoomResponseObjectMessageActorType {
|
enum GetRoomResponseObjectMessageActorType {
|
||||||
@JsonValue('deleted_users')
|
@JsonValue('deleted_users')
|
||||||
deletedUsers,
|
deletedUsers,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'get_room_response.dart';
|
||||||
|
|
||||||
|
typedef NotificationLevel = GetRoomResponseObjectParticipantNotificationLevel;
|
||||||
|
|
||||||
|
/// The levels a user can pick in the UI; [NotificationLevel.defaultLevel]
|
||||||
|
/// is only ever reported by the server, never chosen.
|
||||||
|
const selectableNotificationLevels = [
|
||||||
|
NotificationLevel.alwaysNotify,
|
||||||
|
NotificationLevel.notifyOnMention,
|
||||||
|
NotificationLevel.neverNotify,
|
||||||
|
];
|
||||||
|
|
||||||
|
extension RoomNotificationLevel on GetRoomResponseObject {
|
||||||
|
/// Resolves "default" the way Spreed does: 1:1 chats notify on every
|
||||||
|
/// message, group chats only on mentions (`default_group_notification`).
|
||||||
|
NotificationLevel get effectiveNotificationLevel {
|
||||||
|
if (notificationLevel != NotificationLevel.defaultLevel) {
|
||||||
|
return notificationLevel;
|
||||||
|
}
|
||||||
|
return type == GetRoomResponseObjectConversationType.oneToOne
|
||||||
|
? NotificationLevel.alwaysNotify
|
||||||
|
: NotificationLevel.notifyOnMention;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isMuted =>
|
||||||
|
effectiveNotificationLevel == NotificationLevel.neverNotify;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
|||||||
|
|
||||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||||
|
|
||||||
|
import '../../../../../api/marianumcloud/talk/actions/talk_actions.dart';
|
||||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||||
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||||
@@ -126,6 +127,18 @@ class ChatListBloc
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setNotificationLevel(
|
||||||
|
String token,
|
||||||
|
GetRoomResponseObjectParticipantNotificationLevel level,
|
||||||
|
) async {
|
||||||
|
await SetNotificationLevel(token, level).run();
|
||||||
|
_mutateRoom(token, (r) {
|
||||||
|
if (r.notificationLevel == level) return false;
|
||||||
|
r.notificationLevel = level;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Clears unread too — long-poll only feeds this in for an actively-open chat.
|
/// Clears unread too — long-poll only feeds this in for an actively-open chat.
|
||||||
void applyIncomingMessage(String token, GetChatResponseObject message) {
|
void applyIncomingMessage(String token, GetChatResponseObject message) {
|
||||||
_mutateRoom(token, (r) {
|
_mutateRoom(token, (r) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../../../api/marianumcloud/talk/get_participants/get_participants_cac
|
|||||||
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
|
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
|
||||||
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items_overview_response.dart';
|
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items_overview_response.dart';
|
||||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
|
import '../../../../api/marianumcloud/talk/room/notification_level.dart';
|
||||||
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||||
import '../../../../widget/app_progress_indicator.dart';
|
import '../../../../widget/app_progress_indicator.dart';
|
||||||
import '../../../../widget/async_action_button.dart';
|
import '../../../../widget/async_action_button.dart';
|
||||||
@@ -14,6 +15,7 @@ import '../../../../widget/confirm_dialog.dart';
|
|||||||
import '../../../../widget/large_profile_picture_view.dart';
|
import '../../../../widget/large_profile_picture_view.dart';
|
||||||
import '../../../../widget/user_avatar.dart';
|
import '../../../../widget/user_avatar.dart';
|
||||||
import '../talk_navigator.dart';
|
import '../talk_navigator.dart';
|
||||||
|
import '../widgets/notification_level_sheet.dart';
|
||||||
import 'participants_list_view.dart';
|
import 'participants_list_view.dart';
|
||||||
import 'shared_items_view.dart';
|
import 'shared_items_view.dart';
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ class _ChatInfoState extends State<ChatInfo> {
|
|||||||
GetParticipantsResponse? participants;
|
GetParticipantsResponse? participants;
|
||||||
GetSharedItemsOverviewResponse? _sharesOverview;
|
GetSharedItemsOverviewResponse? _sharesOverview;
|
||||||
late bool _isFavorite;
|
late bool _isFavorite;
|
||||||
|
late NotificationLevel _notificationLevel;
|
||||||
int _avatarVersion = 0;
|
int _avatarVersion = 0;
|
||||||
bool _avatarBusy = false;
|
bool _avatarBusy = false;
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ class _ChatInfoState extends State<ChatInfo> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_isFavorite = widget.room.isFavorite;
|
_isFavorite = widget.room.isFavorite;
|
||||||
|
_notificationLevel = widget.room.effectiveNotificationLevel;
|
||||||
GetParticipantsCache(
|
GetParticipantsCache(
|
||||||
chatToken: widget.room.token,
|
chatToken: widget.room.token,
|
||||||
onUpdate: (GetParticipantsResponse data) {
|
onUpdate: (GetParticipantsResponse data) {
|
||||||
@@ -67,6 +71,16 @@ class _ChatInfoState extends State<ChatInfo> {
|
|||||||
_refreshList();
|
_refreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _editNotificationLevel() async {
|
||||||
|
final level = await showNotificationLevelSheet(
|
||||||
|
context,
|
||||||
|
widget.room,
|
||||||
|
current: _notificationLevel,
|
||||||
|
);
|
||||||
|
if (level == null || !mounted) return;
|
||||||
|
setState(() => _notificationLevel = level);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _confirmLeave() async {
|
Future<void> _confirmLeave() async {
|
||||||
final closed = await showDialog<bool>(
|
final closed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -243,6 +257,11 @@ class _ChatInfoState extends State<ChatInfo> {
|
|||||||
title: const Text('Zu Favoriten hinzufügen'),
|
title: const Text('Zu Favoriten hinzufügen'),
|
||||||
onPressed: _toggleFavorite,
|
onPressed: _toggleFavorite,
|
||||||
),
|
),
|
||||||
|
NotificationLevelTile(
|
||||||
|
level: _notificationLevel,
|
||||||
|
trailing: const Icon(Icons.arrow_right),
|
||||||
|
onTap: _editNotificationLevel,
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
|
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
|
||||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
|
import '../../../../api/marianumcloud/talk/room/notification_level.dart';
|
||||||
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
|
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
|
||||||
import '../../../../extensions/date_time.dart';
|
import '../../../../extensions/date_time.dart';
|
||||||
import '../../../../model/account_data.dart';
|
import '../../../../model/account_data.dart';
|
||||||
@@ -22,6 +23,7 @@ import '../../../../widget/details_bottom_sheet.dart';
|
|||||||
import '../../../../widget/user_avatar.dart';
|
import '../../../../widget/user_avatar.dart';
|
||||||
import '../data/talk_markdown.dart';
|
import '../data/talk_markdown.dart';
|
||||||
import '../talk_navigator.dart';
|
import '../talk_navigator.dart';
|
||||||
|
import 'notification_level_sheet.dart';
|
||||||
|
|
||||||
class ChatTile extends StatefulWidget {
|
class ChatTile extends StatefulWidget {
|
||||||
final GetRoomResponseObject data;
|
final GetRoomResponseObject data;
|
||||||
@@ -151,6 +153,15 @@ class _ChatTileState extends State<ChatTile> {
|
|||||||
semanticLabel: A11yLabels.draft,
|
semanticLabel: A11yLabels.draft,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (widget.data.isMuted) ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Icon(
|
||||||
|
NotificationLevel.neverNotify.icon,
|
||||||
|
size: 15,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
semanticLabel: A11yLabels.muted,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@@ -238,6 +249,17 @@ class _ChatTileState extends State<ChatTile> {
|
|||||||
if (mounted) _refreshList();
|
if (mounted) _refreshList();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
NotificationLevelTile(
|
||||||
|
level: widget.data.effectiveNotificationLevel,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(sheetCtx).pop();
|
||||||
|
showNotificationLevelSheet(
|
||||||
|
context,
|
||||||
|
widget.data,
|
||||||
|
current: widget.data.effectiveNotificationLevel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_outline),
|
leading: const Icon(Icons.delete_outline),
|
||||||
title: const Text('Talk-Chat verlassen'),
|
title: const Text('Talk-Chat verlassen'),
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
|
import '../../../../api/marianumcloud/talk/room/notification_level.dart';
|
||||||
|
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||||
|
import '../../../../widget/async_action_button.dart';
|
||||||
|
import '../../../../widget/details_bottom_sheet.dart';
|
||||||
|
|
||||||
|
extension NotificationLevelPresentation on NotificationLevel {
|
||||||
|
// defaultLevel never reaches the UI (see effectiveNotificationLevel), but
|
||||||
|
// the switch has to stay exhaustive.
|
||||||
|
({String label, String description, IconData icon}) get _presentation =>
|
||||||
|
switch (this) {
|
||||||
|
NotificationLevel.alwaysNotify || NotificationLevel.defaultLevel => (
|
||||||
|
label: 'Alle Nachrichten',
|
||||||
|
description: 'Bei jeder neuen Nachricht',
|
||||||
|
icon: Icons.notifications_outlined,
|
||||||
|
),
|
||||||
|
NotificationLevel.notifyOnMention => (
|
||||||
|
label: 'Nur Erwähnungen',
|
||||||
|
description: 'Nur wenn du erwähnt wirst',
|
||||||
|
icon: Icons.alternate_email,
|
||||||
|
),
|
||||||
|
NotificationLevel.neverNotify => (
|
||||||
|
label: 'Aus',
|
||||||
|
description: 'Keine Benachrichtigungen',
|
||||||
|
icon: Icons.notifications_off_outlined,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
String get label => _presentation.label;
|
||||||
|
String get description => _presentation.description;
|
||||||
|
IconData get icon => _presentation.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lets the user pick the room's Talk notification level. Nextcloud applies
|
||||||
|
/// it server-side, so no push for the room is sent at all when muted.
|
||||||
|
/// Returns the new level, or null when nothing changed.
|
||||||
|
Future<NotificationLevel?> showNotificationLevelSheet(
|
||||||
|
BuildContext context,
|
||||||
|
GetRoomResponseObject room, {
|
||||||
|
required NotificationLevel current,
|
||||||
|
}) async {
|
||||||
|
final bloc = context.read<ChatListBloc>();
|
||||||
|
NotificationLevel? selected;
|
||||||
|
await showDetailsBottomSheet(
|
||||||
|
context,
|
||||||
|
header: ListTile(
|
||||||
|
leading: const Icon(Icons.notifications_outlined),
|
||||||
|
title: const Text('Benachrichtigungen'),
|
||||||
|
subtitle: Text(room.displayName, overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
children: (_) => [
|
||||||
|
for (final level in selectableNotificationLevels)
|
||||||
|
AsyncListTile(
|
||||||
|
leading: Icon(
|
||||||
|
level == current
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
),
|
||||||
|
title: Text(level.label),
|
||||||
|
subtitle: Text(level.description),
|
||||||
|
onPressed: () async {
|
||||||
|
if (level == current) return;
|
||||||
|
await bloc.setNotificationLevel(room.token, level);
|
||||||
|
selected = level;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationLevelTile extends StatelessWidget {
|
||||||
|
final NotificationLevel level;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
const NotificationLevelTile({
|
||||||
|
required this.level,
|
||||||
|
required this.onTap,
|
||||||
|
this.trailing,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ListTile(
|
||||||
|
leading: Icon(level.icon),
|
||||||
|
title: const Text('Benachrichtigungen'),
|
||||||
|
subtitle: Text(level.label),
|
||||||
|
trailing: trailing,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ abstract final class A11yLabels {
|
|||||||
static const unread = 'Ungelesen';
|
static const unread = 'Ungelesen';
|
||||||
static const favorite = 'Favorit';
|
static const favorite = 'Favorit';
|
||||||
static const draft = 'Entwurf';
|
static const draft = 'Entwurf';
|
||||||
|
static const muted = 'Stummgeschaltet';
|
||||||
static const yourReaction = 'deine Reaktion';
|
static const yourReaction = 'deine Reaktion';
|
||||||
|
|
||||||
// Bilder / Kontakte
|
// Bilder / Kontakte
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:marianum_mobile/api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||||
|
import 'package:marianum_mobile/api/marianumcloud/talk/room/get_room_response.dart';
|
||||||
|
import 'package:marianum_mobile/api/marianumcloud/talk/room/notification_level.dart';
|
||||||
|
|
||||||
|
typedef Level = GetRoomResponseObjectParticipantNotificationLevel;
|
||||||
|
typedef RoomType = GetRoomResponseObjectConversationType;
|
||||||
|
|
||||||
|
GetRoomResponseObject _room(RoomType type, Level level) =>
|
||||||
|
GetRoomResponseObject(
|
||||||
|
1,
|
||||||
|
't',
|
||||||
|
type,
|
||||||
|
'n',
|
||||||
|
'd',
|
||||||
|
'',
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
's',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
level,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
GetChatResponseObject(
|
||||||
|
1,
|
||||||
|
't',
|
||||||
|
GetRoomResponseObjectMessageActorType.user,
|
||||||
|
'a',
|
||||||
|
'A',
|
||||||
|
0,
|
||||||
|
'',
|
||||||
|
GetRoomResponseObjectMessageType.comment,
|
||||||
|
true,
|
||||||
|
'r',
|
||||||
|
'm',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Standard wird bei 1:1-Chats zu „Alle Nachrichten"', () {
|
||||||
|
final room = _room(RoomType.oneToOne, Level.defaultLevel);
|
||||||
|
expect(room.effectiveNotificationLevel, Level.alwaysNotify);
|
||||||
|
expect(room.isMuted, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Standard wird bei Gruppen zu „Nur Erwähnungen"', () {
|
||||||
|
expect(
|
||||||
|
_room(RoomType.group, Level.defaultLevel).effectiveNotificationLevel,
|
||||||
|
Level.notifyOnMention,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explizit gesetzter Level bleibt erhalten', () {
|
||||||
|
final room = _room(RoomType.oneToOne, Level.neverNotify);
|
||||||
|
expect(room.effectiveNotificationLevel, Level.neverNotify);
|
||||||
|
expect(room.isMuted, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user