shows room but not really
This commit is contained in:
parent
7b0fea3a07
commit
5f96c8e57f
23 changed files with 885 additions and 805 deletions
|
|
@ -11,6 +11,8 @@ import "package:nexus/controllers/top_level_spaces_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||||
import "package:nexus/models/client_state.dart";
|
import "package:nexus/models/client_state.dart";
|
||||||
import "package:nexus/models/login.dart";
|
import "package:nexus/models/login.dart";
|
||||||
|
import "package:nexus/models/report.dart";
|
||||||
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/sync_data.dart";
|
import "package:nexus/models/sync_data.dart";
|
||||||
import "package:nexus/models/sync_status.dart";
|
import "package:nexus/models/sync_status.dart";
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
|
|
@ -118,6 +120,23 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> leaveRoom(Room room) async {
|
||||||
|
if (room.metadata == null) return;
|
||||||
|
await sendCommand("leave_room", {"room_id": room.metadata!.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reportEvent(Report report) =>
|
||||||
|
sendCommand("report_event", report.toJson());
|
||||||
|
|
||||||
|
Future<void> markRead(Room room) async {
|
||||||
|
if (room.events.isEmpty || room.metadata == null) return;
|
||||||
|
await sendCommand("mark_read", {
|
||||||
|
"room_id": room.metadata?.id,
|
||||||
|
"receipt_type": "m.read",
|
||||||
|
"event_id": room.events.last.eventId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> login(Login login) async {
|
Future<bool> login(Login login) async {
|
||||||
try {
|
try {
|
||||||
await sendCommand("login", login.toJson());
|
await sendCommand("login", login.toJson());
|
||||||
|
|
|
||||||
24
lib/controllers/header_controller.dart
Normal file
24
lib/controllers/header_controller.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import "package:ffi/ffi.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
|
|
||||||
|
class HeaderController extends AsyncNotifier<Map<String, String>> {
|
||||||
|
@override
|
||||||
|
Future<Map<String, String>> build() async {
|
||||||
|
final handle = await ref.watch(ClientController.provider.future);
|
||||||
|
final info = GomuksGetAccountInfo(handle);
|
||||||
|
final headers = {
|
||||||
|
"authorization":
|
||||||
|
"Bearer ${info.access_token.cast<Utf8>().toDartString()}",
|
||||||
|
};
|
||||||
|
|
||||||
|
GomuksFreeAccountInfo(info);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider<HeaderController, Map<String, String>>(
|
||||||
|
HeaderController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,9 @@ import "package:collection/collection.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
import "package:flutter_chat_core/flutter_chat_core.dart" as chat;
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:matrix/matrix.dart";
|
|
||||||
import "package:nexus/controllers/avatar_controller.dart";
|
|
||||||
import "package:nexus/controllers/events_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/event_to_message.dart";
|
|
||||||
import "package:nexus/helpers/extensions/list_to_messages.dart";
|
|
||||||
import "package:fluttertagger/fluttertagger.dart" as tagger;
|
import "package:fluttertagger/fluttertagger.dart" as tagger;
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<ChatController> {
|
class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
final Room room;
|
final Room room;
|
||||||
|
|
@ -16,52 +12,54 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ChatController> build() async {
|
Future<ChatController> build() async {
|
||||||
final timeline = await ref.watch(EventsController.provider(room).future);
|
// final timeline = await ref.watch(EventsController.provider(room).future);
|
||||||
|
|
||||||
ref.onDispose(
|
return InMemoryChatController();
|
||||||
room.client.onTimelineEvent.stream.listen((event) async {
|
|
||||||
if (event.roomId != room.id) return;
|
|
||||||
|
|
||||||
if (event.type == EventTypes.Redaction) {
|
// ref.onDispose(
|
||||||
final controller = await future;
|
// room.client.onTimelineEvent.stream.listen((event) async {
|
||||||
final message = controller.messages.firstWhereOrNull(
|
// if (event.roomId != room.metadata.id) return;
|
||||||
(message) => message.id == event.redacts,
|
|
||||||
);
|
|
||||||
if (message == null) return;
|
|
||||||
|
|
||||||
await controller.removeMessage(message);
|
// if (event.type == "m.room.redaction") {
|
||||||
} else {
|
// final controller = await future;
|
||||||
final message = await event.toMessage(includeEdits: true, timeline);
|
// final message = controller.messages.firstWhereOrNull(
|
||||||
if (event.relationshipType == RelationshipTypes.edit) {
|
// (message) => message.id == event.redacts,
|
||||||
final controller = await future;
|
// );
|
||||||
final oldMessage = controller.messages.firstWhereOrNull(
|
// if (message == null) return;
|
||||||
(element) => element.id == event.relationshipEventId,
|
|
||||||
);
|
|
||||||
if (oldMessage == null || message == null) return;
|
|
||||||
return await updateMessage(
|
|
||||||
oldMessage,
|
|
||||||
message.copyWith(
|
|
||||||
id: oldMessage.id,
|
|
||||||
replyToMessageId: oldMessage.replyToMessageId,
|
|
||||||
metadata: {
|
|
||||||
...(oldMessage.metadata ?? {}),
|
|
||||||
...((message.metadata ?? {}).filterMap(
|
|
||||||
(key, value) => value == null ? null : MapEntry(key, value),
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message != null) {
|
|
||||||
return await insertMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).cancel,
|
|
||||||
);
|
|
||||||
|
|
||||||
return InMemoryChatController(
|
// await controller.removeMessage(message);
|
||||||
messages: await timeline.events.toMessages(room, timeline),
|
// } else {
|
||||||
);
|
// final message = await event.toMessage(includeEdits: true, timeline);
|
||||||
|
// if (event.relationshipType == RelationshipTypes.edit) {
|
||||||
|
// final controller = await future;
|
||||||
|
// final oldMessage = controller.messages.firstWhereOrNull(
|
||||||
|
// (element) => element.id == event.relationshipEventId,
|
||||||
|
// );
|
||||||
|
// if (oldMessage == null || message == null) return;
|
||||||
|
// return await updateMessage(
|
||||||
|
// oldMessage,
|
||||||
|
// message.copyWith(
|
||||||
|
// id: oldMessage.id,
|
||||||
|
// replyToMessageId: oldMessage.replyToMessageId,
|
||||||
|
// metadata: {
|
||||||
|
// ...(oldMessage.metadata ?? {}),
|
||||||
|
// ...((message.metadata ?? {}).filterMap(
|
||||||
|
// (key, value) => value == null ? null : MapEntry(key, value),
|
||||||
|
// )),
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// if (message != null) {
|
||||||
|
// return await insertMessage(message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }).cancel,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return InMemoryChatController(
|
||||||
|
// messages: await timeline.events.toMessages(room, timeline),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertMessage(Message message) async {
|
Future<void> insertMessage(Message message) async {
|
||||||
|
|
@ -79,37 +77,29 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteMessage(Message message, {String? reason}) async {
|
Future<void> deleteMessage(Message message, {String? reason}) async {
|
||||||
final controller = await future;
|
// final controller = await future;
|
||||||
await controller.removeMessage(message);
|
// await controller.removeMessage(message);
|
||||||
await room.redactEvent(message.id, reason: reason);
|
// await room.redactEvent(message.id, reason: reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadOlder() async {
|
Future<void> loadOlder() async {
|
||||||
final currentEvents = await future;
|
// final currentEvents = await future;
|
||||||
await ref.watch(EventsController.provider(room).notifier).prev();
|
// await ref.watch(EventsController.provider(room).notifier).prev();
|
||||||
final timeline = await ref.watch(EventsController.provider(room).future);
|
// final timeline = await ref.watch(EventsController.provider(room).future);
|
||||||
|
|
||||||
final controller = await future;
|
// final controller = await future;
|
||||||
await controller.insertAllMessages(
|
// await controller.insertAllMessages(
|
||||||
await timeline.events
|
// await timeline.events
|
||||||
.where(
|
// .where(
|
||||||
(event) => !currentEvents.messages.any(
|
// (event) => !currentEvents.messages.any(
|
||||||
(existingEvent) => existingEvent.id == event.eventId,
|
// (existingEvent) => existingEvent.id == event.eventId,
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
.toList()
|
// .toList()
|
||||||
.toMessages(room, timeline),
|
// .toMessages(room, timeline),
|
||||||
index: 0,
|
// index: 0,
|
||||||
);
|
// );
|
||||||
ref.notifyListeners();
|
// ref.notifyListeners();
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> markRead() async {
|
|
||||||
if (!room.hasNewMessages) return;
|
|
||||||
final controller = await future;
|
|
||||||
final id = controller.messages.last.id;
|
|
||||||
|
|
||||||
await room.setReadMarker(id, mRead: id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateMessage(Message message, Message newMessage) async =>
|
Future<void> updateMessage(Message message, Message newMessage) async =>
|
||||||
|
|
@ -121,37 +111,37 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
required RelationType relationType,
|
required RelationType relationType,
|
||||||
Message? relation,
|
Message? relation,
|
||||||
}) async {
|
}) async {
|
||||||
var taggedMessage = message;
|
// var taggedMessage = message;
|
||||||
|
|
||||||
for (final tag in tags) {
|
// for (final tag in tags) {
|
||||||
final escaped = RegExp.escape(tag.id);
|
// final escaped = RegExp.escape(tag.id);
|
||||||
final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
|
// final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
|
||||||
|
|
||||||
taggedMessage = taggedMessage.replaceAllMapped(
|
// taggedMessage = taggedMessage.replaceAllMapped(
|
||||||
pattern,
|
// pattern,
|
||||||
(match) => match.group(1)!,
|
// (match) => match.group(1)!,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
await room.sendTextEvent(
|
// await room.sendTextEvent(
|
||||||
taggedMessage,
|
// taggedMessage,
|
||||||
editEventId: relationType == RelationType.edit ? relation?.id : null,
|
// editEventId: relationType == RelationType.edit ? relation?.id : null,
|
||||||
inReplyTo: (relationType == RelationType.reply && relation != null)
|
// inReplyTo: (relationType == RelationType.reply && relation != null)
|
||||||
? await room.getEventById(relation.id)
|
// ? await room.getEventById(relation.id)
|
||||||
: null,
|
// : null,
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<chat.User> resolveUser(String id) async {
|
Future<chat.User> resolveUser(String id) async {
|
||||||
final user = await room.client.getUserProfile(id);
|
// final user = await room.client.getUserProfile(id);
|
||||||
return chat.User(
|
return chat.User(
|
||||||
id: id,
|
id: id,
|
||||||
name: user.displayname,
|
// name: user.displayname,
|
||||||
imageSource: user.avatarUrl == null
|
// imageSource: user.avatarUrl == null
|
||||||
? null
|
// ? null
|
||||||
: (await ref.watch(
|
// : (await ref.watch(
|
||||||
AvatarController.provider(user.avatarUrl!.toString()).future,
|
// AvatarController.provider(user.avatarUrl!.toString()).future,
|
||||||
)).toString(),
|
// )).toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,35 +15,34 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
|
final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
|
||||||
final spaceEdges = ref.watch(SpaceEdgesController.provider);
|
final spaceEdges = ref.watch(SpaceEdgesController.provider);
|
||||||
|
|
||||||
ISet<String> collectChildIds(String spaceId) {
|
final childRoomsBySpaceId = IMap.fromEntries(
|
||||||
ISet<String> result = ISet<String>();
|
|
||||||
void walk(String currentId) {
|
|
||||||
final children = spaceEdges[currentId] ?? IList<SpaceEdge>();
|
|
||||||
for (final edge in children) {
|
|
||||||
final childId = edge.childId;
|
|
||||||
if (!result.contains(childId)) {
|
|
||||||
result = result.add(childId);
|
|
||||||
walk(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walk(spaceId);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
final spaceIdToChildren = IMap.fromEntries(
|
|
||||||
topLevelSpaceIds.map((spaceId) {
|
topLevelSpaceIds.map((spaceId) {
|
||||||
final children = collectChildIds(
|
ISet<String> walk(String currentId) {
|
||||||
|
final children = spaceEdges[currentId] ?? IList<SpaceEdge>();
|
||||||
|
|
||||||
|
return children.fold<ISet<String>>(const ISet.empty(), (acc, edge) {
|
||||||
|
final childId = edge.childId;
|
||||||
|
final isSpace = spaceEdges.containsKey(childId);
|
||||||
|
|
||||||
|
return acc
|
||||||
|
.addAll(!isSpace ? ISet([childId]) : const ISet.empty())
|
||||||
|
.addAll(isSpace ? walk(childId) : const ISet.empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapEntry(
|
||||||
spaceId,
|
spaceId,
|
||||||
).map((id) => rooms[id]).nonNulls.toIList();
|
walk(spaceId).map((id) => rooms[id]).nonNulls.toIList(),
|
||||||
return MapEntry(spaceId, children);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
final allNestedRoomIds = spaceIdToChildren.values
|
final allNestedRoomIds = childRoomsBySpaceId.values
|
||||||
.expand((l) => l)
|
.expand((l) => l)
|
||||||
.map((r) => rooms.entries.firstWhere((e) => e.value == r).key)
|
.map(
|
||||||
|
(room) =>
|
||||||
|
rooms.entries.firstWhere((entry) => entry.value == room).key,
|
||||||
|
)
|
||||||
.toISet();
|
.toISet();
|
||||||
|
|
||||||
final dmRooms = rooms.values
|
final dmRooms = rooms.values
|
||||||
|
|
@ -55,7 +54,8 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
(e) =>
|
(e) =>
|
||||||
e.value.metadata?.dmUserId == null &&
|
e.value.metadata?.dmUserId == null &&
|
||||||
!allNestedRoomIds.contains(e.key) &&
|
!allNestedRoomIds.contains(e.key) &&
|
||||||
!topLevelSpaceIds.contains(e.key),
|
!topLevelSpaceIds.contains(e.key) &&
|
||||||
|
!spaceEdges.containsKey(e.key),
|
||||||
)
|
)
|
||||||
.map((e) => e.value)
|
.map((e) => e.value)
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
@ -65,7 +65,7 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
final room = rooms[id];
|
final room = rooms[id];
|
||||||
if (room == null) return null;
|
if (room == null) return null;
|
||||||
|
|
||||||
final children = spaceIdToChildren[id] ?? IList<Room>();
|
final children = childRoomsBySpaceId[id] ?? IList<Room>();
|
||||||
return Space(
|
return Space(
|
||||||
id: id,
|
id: id,
|
||||||
title: room.metadata?.name ?? "Unnamed Room",
|
title: room.metadata?.name ?? "Unnamed Room",
|
||||||
|
|
|
||||||
7
lib/helpers/extensions/get_headers.dart
Normal file
7
lib/helpers/extensions/get_headers.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/header_controller.dart";
|
||||||
|
|
||||||
|
extension GetHeaders on WidgetRef {
|
||||||
|
Map<String, String> get headers =>
|
||||||
|
watch(HeaderController.provider).requireValue;
|
||||||
|
}
|
||||||
40
lib/helpers/extensions/link_to_mention.dart
Normal file
40
lib/helpers/extensions/link_to_mention.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
extension LinkToMention on String {
|
||||||
|
/// Extracts a Matrix identifier from this string.
|
||||||
|
///
|
||||||
|
/// Supports:
|
||||||
|
/// - https://matrix.to/#/...
|
||||||
|
/// - matrix:roomid/...
|
||||||
|
/// - matrix:r/...
|
||||||
|
/// - matrix:u/...
|
||||||
|
///
|
||||||
|
/// Returns the decoded identifier (e.g. "#room:matrix.org")
|
||||||
|
/// or null if this is not a Matrix link.
|
||||||
|
String? get mention {
|
||||||
|
final trimmed = trim();
|
||||||
|
|
||||||
|
final matrixTo = RegExp(
|
||||||
|
r"^https?://matrix\.to/#/([^/?#]+)",
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final matrixToMatch = matrixTo.firstMatch(trimmed);
|
||||||
|
if (matrixToMatch != null) {
|
||||||
|
return Uri.decodeComponent(matrixToMatch.group(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.toLowerCase().startsWith("matrix:")) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(trimmed);
|
||||||
|
|
||||||
|
if (uri.pathSegments.isNotEmpty) {
|
||||||
|
final identifier = uri.pathSegments.last;
|
||||||
|
if (identifier.isNotEmpty) {
|
||||||
|
return Uri.decodeComponent(identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import "package:flutter/foundation.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_controller.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
|
import "package:nexus/controllers/header_controller.dart";
|
||||||
import "package:nexus/controllers/multi_provider_controller.dart";
|
import "package:nexus/controllers/multi_provider_controller.dart";
|
||||||
import "package:nexus/controllers/shared_prefs_controller.dart";
|
import "package:nexus/controllers/shared_prefs_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
|
@ -108,6 +109,7 @@ class App extends StatelessWidget {
|
||||||
IListConst([
|
IListConst([
|
||||||
SharedPrefsController.provider,
|
SharedPrefsController.provider,
|
||||||
ClientController.provider,
|
ClientController.provider,
|
||||||
|
HeaderController.provider,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ abstract class ClientState with _$ClientState {
|
||||||
required bool isInitialized,
|
required bool isInitialized,
|
||||||
required bool isLoggedIn,
|
required bool isLoggedIn,
|
||||||
required bool isVerified,
|
required bool isVerified,
|
||||||
|
required String userId,
|
||||||
}) = _ClientState;
|
}) = _ClientState;
|
||||||
|
|
||||||
factory ClientState.fromJson(Map<String, Object?> json) =>
|
factory ClientState.fromJson(Map<String, Object?> json) =>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ abstract class Event with _$Event {
|
||||||
String? transactionId,
|
String? transactionId,
|
||||||
String? redactedBy,
|
String? redactedBy,
|
||||||
String? relatesTo,
|
String? relatesTo,
|
||||||
String? relatesType,
|
@JsonKey(name: "relates_type") String? relationType,
|
||||||
String? decryptionError,
|
String? decryptionError,
|
||||||
String? sendError,
|
String? sendError,
|
||||||
@Default(IMap.empty()) IMap<String, int> reactions,
|
@Default(IMap.empty()) IMap<String, int> reactions,
|
||||||
|
|
|
||||||
1
lib/models/relation_type.dart
Normal file
1
lib/models/relation_type.dart
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
enum RelationType { edit, reply }
|
||||||
14
lib/models/report.dart
Normal file
14
lib/models/report.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "report.freezed.dart";
|
||||||
|
part "report.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class Report with _$Report {
|
||||||
|
const factory Report({
|
||||||
|
required String roomId,
|
||||||
|
required String eventId,
|
||||||
|
String? reason,
|
||||||
|
}) = _Report;
|
||||||
|
|
||||||
|
factory Report.fromJson(Map<String, Object?> json) => _$ReportFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,9 @@ abstract class RoomMetadata with _$RoomMetadata {
|
||||||
required bool hasMemberList,
|
required bool hasMemberList,
|
||||||
@JsonKey(name: "preview_event_rowid") required int previewEventRowID,
|
@JsonKey(name: "preview_event_rowid") required int previewEventRowID,
|
||||||
@EpochDateTimeConverter() required DateTime sortingTimestamp,
|
@EpochDateTimeConverter() required DateTime sortingTimestamp,
|
||||||
@Default(false) bool markedUnread,
|
required int unreadHighlights,
|
||||||
|
required int unreadNotifications,
|
||||||
|
required int unreadMessages,
|
||||||
}) = _RoomMetadata;
|
}) = _RoomMetadata;
|
||||||
|
|
||||||
factory RoomMetadata.fromJson(Map<String, Object?> json) =>
|
factory RoomMetadata.fromJson(Map<String, Object?> json) =>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:nexus/widgets/chat_page/sidebar.dart";
|
import "package:nexus/widgets/chat_page/sidebar.dart";
|
||||||
// import "package:nexus/widgets/chat_page/room_chat.dart";
|
import "package:nexus/widgets/chat_page/room_chat.dart";
|
||||||
|
|
||||||
class ChatPage extends StatelessWidget {
|
class ChatPage extends StatelessWidget {
|
||||||
const ChatPage({super.key});
|
const ChatPage({super.key});
|
||||||
|
|
@ -16,12 +16,12 @@ class ChatPage extends StatelessWidget {
|
||||||
builder: (context) => Row(
|
builder: (context) => Row(
|
||||||
children: [
|
children: [
|
||||||
if (isDesktop) Sidebar(),
|
if (isDesktop) Sidebar(),
|
||||||
// Expanded(
|
Expanded(
|
||||||
// child: RoomChat(
|
child: RoomChat(
|
||||||
// isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
// showMembersByDefault: showMembersByDefault,
|
showMembersByDefault: showMembersByDefault,
|
||||||
// ),
|
),
|
||||||
// ),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ class AvatarOrHash extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Widget? fallback;
|
final Widget? fallback;
|
||||||
final bool hasBadge;
|
final bool hasBadge;
|
||||||
|
final int badgeNumber;
|
||||||
final double height;
|
final double height;
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
const AvatarOrHash(
|
const AvatarOrHash(
|
||||||
this.avatar,
|
this.avatar,
|
||||||
this.title, {
|
this.title, {
|
||||||
this.fallback,
|
this.fallback,
|
||||||
|
this.badgeNumber = 0,
|
||||||
this.hasBadge = false,
|
this.hasBadge = false,
|
||||||
this.height = 24,
|
this.height = 24,
|
||||||
required this.headers,
|
required this.headers,
|
||||||
|
|
@ -30,6 +32,7 @@ class AvatarOrHash extends StatelessWidget {
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Badge(
|
child: Badge(
|
||||||
isLabelVisible: hasBadge,
|
isLabelVisible: hasBadge,
|
||||||
|
label: badgeNumber != 0 ? Text(badgeNumber.toString()) : null,
|
||||||
smallSize: 12,
|
smallSize: 12,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:fluttertagger/fluttertagger.dart";
|
import "package:fluttertagger/fluttertagger.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:matrix/matrix.dart";
|
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/chat_page/mention_overlay.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/widgets/chat_page/relation_preview.dart";
|
import "package:nexus/widgets/chat_page/relation_preview.dart";
|
||||||
|
|
||||||
class ChatBox extends HookConsumerWidget {
|
class ChatBox extends HookConsumerWidget {
|
||||||
|
|
@ -94,7 +93,6 @@ class ChatBox extends HookConsumerWidget {
|
||||||
relatedMessage: relatedMessage,
|
relatedMessage: relatedMessage,
|
||||||
relationType: relationType,
|
relationType: relationType,
|
||||||
onDismiss: onDismiss,
|
onDismiss: onDismiss,
|
||||||
room: room,
|
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
|
@ -105,20 +103,21 @@ class ChatBox extends HookConsumerWidget {
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder: (context) => [],
|
itemBuilder: (context) => [],
|
||||||
icon: Icon(Icons.add),
|
icon: Icon(Icons.add),
|
||||||
enabled: room.canSendDefaultMessages,
|
// enabled: room.canSendDefaultMessages, TODO: Permissions check
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlutterTagger(
|
child: FlutterTagger(
|
||||||
triggerStrategy: TriggerStrategy.eager,
|
triggerStrategy: TriggerStrategy.eager,
|
||||||
overlay: MentionOverlay(
|
overlay: SizedBox.shrink(),
|
||||||
room,
|
// MentionOverlay( TODO: Fix
|
||||||
query: query.value,
|
// room,
|
||||||
triggerCharacter: triggerCharacter.value,
|
// query: query.value,
|
||||||
addTag: ({required id, required name}) {
|
// triggerCharacter: triggerCharacter.value,
|
||||||
controller.value.addTag(id: id, name: name);
|
// addTag: ({required id, required name}) {
|
||||||
node.requestFocus();
|
// controller.value.addTag(id: id, name: name);
|
||||||
},
|
// node.requestFocus();
|
||||||
),
|
// },
|
||||||
|
// ),
|
||||||
controller: controller.value,
|
controller: controller.value,
|
||||||
onSearch: (newQuery, newTriggerCharacter) {
|
onSearch: (newQuery, newTriggerCharacter) {
|
||||||
triggerCharacter.value = newTriggerCharacter;
|
triggerCharacter.value = newTriggerCharacter;
|
||||||
|
|
@ -126,13 +125,13 @@ class ChatBox extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
triggerCharacterAndStyles: {"@": style, "#": style},
|
triggerCharacterAndStyles: {"@": style, "#": style},
|
||||||
builder: (context, key) => TextFormField(
|
builder: (context, key) => TextFormField(
|
||||||
enabled: room.canSendDefaultMessages,
|
// enabled: room.canSendDefaultMessages,
|
||||||
maxLines: 12,
|
maxLines: 12,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: room.canSendDefaultMessages
|
// hintText: room.canSendDefaultMessages
|
||||||
? "Your message here..."
|
// ? "Your message here..."
|
||||||
: "You don't have permission to send messages in this room...",
|
// : "You don't have permission to send messages in this room...",
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
controller: controller.value,
|
controller: controller.value,
|
||||||
|
|
@ -143,7 +142,8 @@ class ChatBox extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: room.canSendDefaultMessages ? send : null,
|
onPressed: send,
|
||||||
|
// onPressed: room.canSendDefaultMessages ? send : null,
|
||||||
icon: Icon(Icons.send),
|
icon: Icon(Icons.send),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,16 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
import "package:nexus/controllers/thumbnail_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/models/image_data.dart";
|
|
||||||
import "package:nexus/widgets/chat_page/html/mention_chip.dart";
|
import "package:nexus/widgets/chat_page/html/mention_chip.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/spoiler_text.dart";
|
import "package:nexus/widgets/chat_page/html/spoiler_text.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/quoted.dart";
|
import "package:nexus/widgets/chat_page/html/quoted.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
|
||||||
|
|
||||||
class Html extends ConsumerWidget {
|
class Html extends ConsumerWidget {
|
||||||
final String html;
|
final String html;
|
||||||
final Client client;
|
const Html(this.html, {super.key});
|
||||||
const Html(this.html, {required this.client, super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
|
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
|
||||||
|
|
@ -38,61 +33,60 @@ class Html extends ConsumerWidget {
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
"blockquote" => Quoted(Html(element.innerHtml, client: client)),
|
"blockquote" => Quoted(Html(element.innerHtml)),
|
||||||
|
|
||||||
"a" =>
|
"a" =>
|
||||||
element.attributes["href"]?.parseIdentifierIntoParts() == null
|
element.attributes["href"]?.mention == null
|
||||||
? null
|
? null
|
||||||
: InlineCustomWidget(child: MentionChip(element.text)),
|
: InlineCustomWidget(child: MentionChip(element.text)),
|
||||||
|
|
||||||
"img" =>
|
// "img" => TODO: Img support
|
||||||
element.attributes["src"] == null
|
// element.attributes["src"] == null
|
||||||
? null
|
// ? null
|
||||||
: Consumer(
|
// : Consumer(
|
||||||
builder: (_, ref, _) => ref
|
// builder: (_, ref, _) => ref
|
||||||
.watch(
|
// .watch(
|
||||||
ThumbnailController.provider(
|
// ThumbnailController.provider(
|
||||||
ImageData(
|
// ImageData(
|
||||||
uri: element.attributes["src"]!,
|
// uri: element.attributes["src"]!,
|
||||||
height: height,
|
// height: height,
|
||||||
width: width,
|
// width: width,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
.when(
|
// .when(
|
||||||
data: (uri) {
|
// data: (uri) {
|
||||||
if (uri == null) return SizedBox.shrink();
|
// if (uri == null) return SizedBox.shrink();
|
||||||
|
|
||||||
return InlineCustomWidget(
|
|
||||||
child: Image.network(
|
|
||||||
uri,
|
|
||||||
headers: client.headers,
|
|
||||||
errorBuilder: (_, error, _) => Text(
|
|
||||||
"Image Failed to Load",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
height: height.toDouble(),
|
|
||||||
width: width?.toDouble(),
|
|
||||||
loadingBuilder: (_, child, loadingProgress) =>
|
|
||||||
loadingProgress == null
|
|
||||||
? child
|
|
||||||
: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: ErrorDialog.new,
|
|
||||||
loading: () => InlineCustomWidget(
|
|
||||||
child: SizedBox(
|
|
||||||
width: width?.toDouble(),
|
|
||||||
height: height.toDouble(),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
|
// return InlineCustomWidget(
|
||||||
|
// child: Image.network(
|
||||||
|
// uri,
|
||||||
|
// headers: client.headers,
|
||||||
|
// errorBuilder: (_, error, _) => Text(
|
||||||
|
// "Image Failed to Load",
|
||||||
|
// style: TextStyle(
|
||||||
|
// color: Theme.of(context).colorScheme.error,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// height: height.toDouble(),
|
||||||
|
// width: width?.toDouble(),
|
||||||
|
// loadingBuilder: (_, child, loadingProgress) =>
|
||||||
|
// loadingProgress == null
|
||||||
|
// ? child
|
||||||
|
// : CircularProgressIndicator(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// error: ErrorDialog.new,
|
||||||
|
// loading: () => InlineCustomWidget(
|
||||||
|
// child: SizedBox(
|
||||||
|
// width: width?.toDouble(),
|
||||||
|
// height: height.toDouble(),
|
||||||
|
// child: CircularProgressIndicator(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
("del" ||
|
("del" ||
|
||||||
"h1" ||
|
"h1" ||
|
||||||
"h2" ||
|
"h2" ||
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
|
|
||||||
class MentionChip extends StatelessWidget {
|
class MentionChip extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
|
|
@ -8,7 +8,7 @@ class MentionChip extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ActionChip(
|
Widget build(BuildContext context) => ActionChip(
|
||||||
label: Text(
|
label: Text(
|
||||||
label.parseIdentifierIntoParts()?.primaryIdentifier ?? label,
|
label.mention ?? label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:matrix/matrix.dart";
|
|
||||||
import "package:nexus/controllers/avatar_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
|
||||||
|
|
||||||
class RelationPreview extends ConsumerWidget {
|
class RelationPreview extends ConsumerWidget {
|
||||||
final Message? relatedMessage;
|
final Message? relatedMessage;
|
||||||
final RelationType relationType;
|
final RelationType relationType;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
final Room room;
|
|
||||||
const RelationPreview({
|
const RelationPreview({
|
||||||
required this.relatedMessage,
|
required this.relatedMessage,
|
||||||
required this.relationType,
|
required this.relationType,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
required this.room,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -37,18 +31,18 @@ class RelationPreview extends ConsumerWidget {
|
||||||
"Editing message:",
|
"Editing message:",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
AvatarOrHash(
|
// AvatarOrHash(
|
||||||
ref
|
// ref
|
||||||
.watch(
|
// .watch(
|
||||||
AvatarController.provider(
|
// AvatarController.provider(
|
||||||
relatedMessage!.metadata!["avatarUrl"],
|
// relatedMessage!.metadata!["avatarUrl"],
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
.whenOrNull(data: (data) => data),
|
// .whenOrNull(data: (data) => data),
|
||||||
relatedMessage!.metadata!["displayName"].toString(),
|
// relatedMessage!.metadata!["displayName"].toString(),
|
||||||
headers: room.client.headers,
|
// headers: room.client.headers,
|
||||||
height: 16,
|
// height: 16,
|
||||||
),
|
// ),
|
||||||
Text(
|
Text(
|
||||||
relatedMessage!.metadata?["displayName"] ??
|
relatedMessage!.metadata?["displayName"] ??
|
||||||
relatedMessage!.authorId,
|
relatedMessage!.authorId,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/full_room.dart";
|
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/chat_page/room_menu.dart";
|
import "package:nexus/widgets/chat_page/room_menu.dart";
|
||||||
|
|
||||||
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final bool isDesktop;
|
final bool isDesktop;
|
||||||
final FullRoom room;
|
final Room room;
|
||||||
final void Function(BuildContext context) onOpenMemberList;
|
final void Function(BuildContext context) onOpenMemberList;
|
||||||
final void Function(BuildContext context) onOpenDrawer;
|
final void Function(BuildContext context) onOpenDrawer;
|
||||||
const RoomAppbar(
|
const RoomAppbar(
|
||||||
|
|
@ -24,22 +24,27 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Appbar(
|
Widget build(BuildContext context) => Appbar(
|
||||||
leading: isDesktop
|
leading: isDesktop
|
||||||
? AvatarOrHash(
|
? null
|
||||||
room.avatar,
|
// AvatarOrHash( TODO: Images
|
||||||
room.title,
|
// room.avatar,
|
||||||
height: 24,
|
// room.title,
|
||||||
fallback: Icon(Icons.numbers),
|
// height: 24,
|
||||||
headers: room.roomData.client.headers,
|
// fallback: Icon(Icons.numbers),
|
||||||
)
|
// headers: room.roomData.client.headers,
|
||||||
|
// )
|
||||||
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(room.title, overflow: TextOverflow.ellipsis, maxLines: 1),
|
|
||||||
if (room.roomData.topic.isNotEmpty)
|
|
||||||
Text(
|
Text(
|
||||||
room.roomData.topic,
|
room.metadata?.name ?? "Unnamed Room",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
if (room.metadata?.topic?.isNotEmpty == true)
|
||||||
|
Text(
|
||||||
|
room.metadata!.topic!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
|
@ -54,7 +59,7 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
onPressed: () => onOpenMemberList(context),
|
onPressed: () => onOpenMemberList(context),
|
||||||
icon: Icon(Icons.people),
|
icon: Icon(Icons.people),
|
||||||
),
|
),
|
||||||
RoomMenu(room.roomData),
|
RoomMenu(room),
|
||||||
],
|
].toIList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
|
||||||
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
|
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
|
||||||
import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
|
import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/controllers/cross_cache_controller.dart";
|
import "package:nexus/controllers/cross_cache_controller.dart";
|
||||||
import "package:nexus/controllers/selected_room_controller.dart";
|
import "package:nexus/controllers/selected_room_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
|
|
@ -16,9 +18,9 @@ import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
|
import "package:nexus/models/report.dart";
|
||||||
import "package:nexus/widgets/chat_page/chat_box.dart";
|
import "package:nexus/widgets/chat_page/chat_box.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/html.dart";
|
import "package:nexus/widgets/chat_page/html/html.dart";
|
||||||
import "package:nexus/widgets/chat_page/member_list.dart";
|
|
||||||
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
||||||
import "package:nexus/widgets/chat_page/top_widget.dart";
|
import "package:nexus/widgets/chat_page/top_widget.dart";
|
||||||
import "package:nexus/widgets/form_text_input.dart";
|
import "package:nexus/widgets/form_text_input.dart";
|
||||||
|
|
@ -37,17 +39,16 @@ class RoomChat extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final replyToMessage = useState<Message?>(null);
|
final replyToMessage = useState<Message?>(null);
|
||||||
final memberListOpened = useState<bool>(showMembersByDefault);
|
final memberListOpened = useState<bool>(showMembersByDefault);
|
||||||
final relationType = useState(RelationType.reply);
|
final relationType = useState(RelationType.reply);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final danger = theme.colorScheme.error;
|
final danger = theme.colorScheme.error;
|
||||||
|
final room = ref.watch(SelectedRoomController.provider);
|
||||||
|
final userId = ref.watch(ClientStateController.provider)?.userId;
|
||||||
|
|
||||||
return ref
|
if (room == null || userId == null) {
|
||||||
.watch(SelectedRoomController.provider)
|
|
||||||
.betterWhen(
|
|
||||||
data: (room) {
|
|
||||||
if (room == null) {
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Nothing to see here...",
|
"Nothing to see here...",
|
||||||
|
|
@ -55,44 +56,34 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final controllerProvider = RoomChatController.provider(
|
final controllerProvider = RoomChatController.provider(room);
|
||||||
room.roomData,
|
|
||||||
);
|
|
||||||
final notifier = ref.watch(controllerProvider.notifier);
|
final notifier = ref.watch(controllerProvider.notifier);
|
||||||
|
|
||||||
List<PopupMenuEntry> getMessageOptions(Message message) => [
|
List<PopupMenuEntry> getMessageOptions(Message message) {
|
||||||
|
final isSentByMe = message.authorId == userId;
|
||||||
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
replyToMessage.value = message;
|
replyToMessage.value = message;
|
||||||
relationType.value = RelationType.reply;
|
relationType.value = RelationType.reply;
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
|
||||||
leading: Icon(Icons.reply),
|
|
||||||
title: Text("Reply"),
|
|
||||||
),
|
),
|
||||||
),
|
if (message is TextMessage && isSentByMe)
|
||||||
// Should check if is state event (has state_key), if so, don't show edit option
|
|
||||||
if (message is TextMessage &&
|
|
||||||
message.authorId == room.roomData.client.userID)
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
replyToMessage.value = message;
|
replyToMessage.value = message;
|
||||||
relationType.value = RelationType.edit;
|
relationType.value = RelationType.edit;
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
|
||||||
leading: Icon(Icons.edit),
|
|
||||||
title: Text("Edit"),
|
|
||||||
),
|
),
|
||||||
),
|
if (isSentByMe) // TODO: Or if user has permission to redact others' messages
|
||||||
if (message.authorId == room.roomData.client.userID ||
|
|
||||||
room.roomData.canRedact)
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () => showDialog(
|
onTap: () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => HookBuilder(
|
builder: (context) => HookBuilder(
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
final deleteReasonController =
|
final deleteReasonController = useTextEditingController();
|
||||||
useTextEditingController();
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("Delete Message"),
|
title: Text("Delete Message"),
|
||||||
content: Column(
|
content: Column(
|
||||||
|
|
@ -131,18 +122,32 @@ class RoomChat extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(leading: Icon(Icons.delete), title: Text("Delete")),
|
||||||
leading: Icon(Icons.delete),
|
|
||||||
title: Text("Delete"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () => showDialog(
|
onTap: () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => HookBuilder(
|
||||||
|
builder: (_) {
|
||||||
|
final reasonController = useTextEditingController();
|
||||||
|
return AlertDialog(
|
||||||
title: Text("Report"),
|
title: Text("Report"),
|
||||||
content: Text(
|
content: Column(
|
||||||
"Report this message to your server administrators, who can take action like banning that user or blocking that server from federating.",
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Report this event to your server administrators, who can take action like banning this server or room.",
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12),
|
||||||
|
FormTextInput(
|
||||||
|
required: false,
|
||||||
|
capitalize: true,
|
||||||
|
controller: reasonController,
|
||||||
|
title: "Reason for report (optional)",
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -151,15 +156,23 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
room.roomData.client.reportEvent(
|
if (room.metadata == null) return;
|
||||||
room.roomData.id,
|
client.reportEvent(
|
||||||
message.id,
|
Report(
|
||||||
|
roomId: room.metadata!.id,
|
||||||
|
eventId: message.id,
|
||||||
|
reason: reasonController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: reasonController.text,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text("Report"),
|
child: Text("Report"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
|
@ -168,6 +181,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: RoomAppbar(
|
appBar: RoomAppbar(
|
||||||
|
|
@ -189,17 +203,11 @@ class RoomChat extends HookConsumerWidget {
|
||||||
.watch(controllerProvider)
|
.watch(controllerProvider)
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (controller) => Chat(
|
data: (controller) => Chat(
|
||||||
currentUserId: room.roomData.client.userID!,
|
currentUserId: userId,
|
||||||
theme: ChatTheme.fromThemeData(theme)
|
theme: ChatTheme.fromThemeData(theme).copyWith(
|
||||||
.copyWith(
|
colors: ChatColors.fromThemeData(theme).copyWith(
|
||||||
colors: ChatColors.fromThemeData(theme)
|
primary: theme.colorScheme.primaryContainer,
|
||||||
.copyWith(
|
onPrimary: theme.colorScheme.onPrimaryContainer,
|
||||||
primary: theme
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer,
|
|
||||||
onPrimary: theme
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onMessageSecondaryTap:
|
onMessageSecondaryTap:
|
||||||
|
|
@ -211,8 +219,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
}) => details?.globalPosition == null
|
}) => details?.globalPosition == null
|
||||||
? null
|
? null
|
||||||
: context.showContextMenu(
|
: context.showContextMenu(
|
||||||
globalPosition:
|
globalPosition: details!.globalPosition,
|
||||||
details!.globalPosition,
|
|
||||||
children: getMessageOptions(message),
|
children: getMessageOptions(message),
|
||||||
),
|
),
|
||||||
onMessageLongPress:
|
onMessageLongPress:
|
||||||
|
|
@ -236,21 +243,16 @@ class RoomChat extends HookConsumerWidget {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => Dialog(
|
builder: (_) => Dialog(
|
||||||
backgroundColor:
|
backgroundColor: Colors.transparent,
|
||||||
Colors.transparent,
|
|
||||||
insetPadding: EdgeInsets.all(64),
|
insetPadding: EdgeInsets.all(64),
|
||||||
child: InteractiveViewer(
|
child: InteractiveViewer(
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedNetworkImage(
|
image: CachedNetworkImage(
|
||||||
message.source,
|
message.source,
|
||||||
ref.watch(
|
ref.watch(
|
||||||
CrossCacheController
|
CrossCacheController.provider,
|
||||||
.provider,
|
|
||||||
),
|
),
|
||||||
headers: room
|
headers: ref.headers,
|
||||||
.roomData
|
|
||||||
.client
|
|
||||||
.headers,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -264,17 +266,17 @@ class RoomChat extends HookConsumerWidget {
|
||||||
ChatAnimatedList(
|
ChatAnimatedList(
|
||||||
itemBuilder: itemBuilder,
|
itemBuilder: itemBuilder,
|
||||||
onEndReached: notifier.loadOlder,
|
onEndReached: notifier.loadOlder,
|
||||||
onStartReached: notifier.markRead,
|
onStartReached: () => client.markRead(room),
|
||||||
bottomPadding: 72,
|
bottomPadding: 72,
|
||||||
),
|
),
|
||||||
composerBuilder: (_) => ChatBox(
|
composerBuilder: (_) => ChatBox(
|
||||||
relationType: relationType.value,
|
relationType: relationType.value,
|
||||||
relatedMessage: replyToMessage.value,
|
relatedMessage: replyToMessage.value,
|
||||||
onDismiss: () =>
|
onDismiss: () => replyToMessage.value = null,
|
||||||
replyToMessage.value = null,
|
room: room,
|
||||||
room: room.roomData,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// TODO: Polls
|
||||||
// customMessageBuilder:
|
// customMessageBuilder:
|
||||||
// (
|
// (
|
||||||
// context,
|
// context,
|
||||||
|
|
@ -317,7 +319,6 @@ class RoomChat extends HookConsumerWidget {
|
||||||
// groupStatus: groupStatus,
|
// groupStatus: groupStatus,
|
||||||
// ),
|
// ),
|
||||||
|
|
||||||
// // TODO: Make this actually work
|
|
||||||
// DynamicPolls(
|
// DynamicPolls(
|
||||||
// startDate: DateTime.now(),
|
// startDate: DateTime.now(),
|
||||||
// endDate: DateTime.now(),
|
// endDate: DateTime.now(),
|
||||||
|
|
@ -395,8 +396,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
(m) {
|
(m) {
|
||||||
// If it's already an <a> tag, leave it unchanged
|
// If it's already an <a> tag, leave it unchanged
|
||||||
if (m.group(1) !=
|
if (m.group(1) != null) {
|
||||||
null) {
|
|
||||||
return m.group(1)!;
|
return m.group(1)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,48 +406,38 @@ class RoomChat extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replaceAll("\n", "<br/>"),
|
.replaceAll("\n", "<br/>"),
|
||||||
client: room.roomData.client,
|
|
||||||
),
|
),
|
||||||
if (message.editedAt != null)
|
if (message.editedAt != null)
|
||||||
Text(
|
Text(
|
||||||
"(edited)",
|
"(edited)",
|
||||||
style: theme
|
style: theme.textTheme.labelSmall,
|
||||||
.textTheme
|
|
||||||
.labelSmall,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
topWidget: TopWidget(
|
topWidget: TopWidget(
|
||||||
message,
|
message,
|
||||||
headers:
|
|
||||||
room.roomData.client.headers,
|
|
||||||
groupStatus: groupStatus,
|
groupStatus: groupStatus,
|
||||||
),
|
),
|
||||||
message: message,
|
message: message,
|
||||||
showTime: true,
|
showTime: true,
|
||||||
index: index,
|
index: index,
|
||||||
),
|
),
|
||||||
linkPreviewBuilder:
|
linkPreviewBuilder: (_, message, isSentByMe) =>
|
||||||
(_, message, isSentByMe) => LinkPreview(
|
LinkPreview(
|
||||||
text: message.text,
|
text: message.text,
|
||||||
backgroundColor: isSentByMe
|
backgroundColor: isSentByMe
|
||||||
? theme.colorScheme.inversePrimary
|
? theme.colorScheme.inversePrimary
|
||||||
: theme
|
: theme.colorScheme.surfaceContainerLow,
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerLow,
|
|
||||||
insidePadding: EdgeInsets.symmetric(
|
insidePadding: EdgeInsets.symmetric(
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
linkPreviewData:
|
linkPreviewData: message.linkPreviewData,
|
||||||
message.linkPreviewData,
|
onLinkPreviewDataFetched: (linkPreviewData) =>
|
||||||
onLinkPreviewDataFetched:
|
|
||||||
(linkPreviewData) =>
|
|
||||||
notifier.updateMessage(
|
notifier.updateMessage(
|
||||||
message,
|
message,
|
||||||
message.copyWith(
|
message.copyWith(
|
||||||
linkPreviewData:
|
linkPreviewData: linkPreviewData,
|
||||||
linkPreviewData,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -461,24 +451,15 @@ class RoomChat extends HookConsumerWidget {
|
||||||
}) => FlyerChatImageMessage(
|
}) => FlyerChatImageMessage(
|
||||||
topWidget: TopWidget(
|
topWidget: TopWidget(
|
||||||
message,
|
message,
|
||||||
headers:
|
|
||||||
room.roomData.client.headers,
|
|
||||||
groupStatus: groupStatus,
|
groupStatus: groupStatus,
|
||||||
alwaysShow: true,
|
alwaysShow: true,
|
||||||
),
|
),
|
||||||
customImageProvider:
|
customImageProvider: CachedNetworkImage(
|
||||||
CachedNetworkImage(
|
|
||||||
message.source,
|
message.source,
|
||||||
ref.watch(
|
ref.watch(CrossCacheController.provider),
|
||||||
CrossCacheController.provider,
|
headers: ref.headers,
|
||||||
),
|
),
|
||||||
headers: room
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
.roomData
|
|
||||||
.client
|
|
||||||
.headers,
|
|
||||||
),
|
|
||||||
errorBuilder:
|
|
||||||
(context, error, stackTrace) =>
|
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Image Failed to Load",
|
"Image Failed to Load",
|
||||||
|
|
@ -511,8 +492,6 @@ class RoomChat extends HookConsumerWidget {
|
||||||
child: FlyerChatFileMessage(
|
child: FlyerChatFileMessage(
|
||||||
topWidget: TopWidget(
|
topWidget: TopWidget(
|
||||||
message,
|
message,
|
||||||
headers:
|
|
||||||
room.roomData.client.headers,
|
|
||||||
groupStatus: groupStatus,
|
groupStatus: groupStatus,
|
||||||
),
|
),
|
||||||
message: message,
|
message: message,
|
||||||
|
|
@ -539,8 +518,9 @@ class RoomChat extends HookConsumerWidget {
|
||||||
MessageGroupStatus? groupStatus,
|
MessageGroupStatus? groupStatus,
|
||||||
}) => Text(
|
}) => Text(
|
||||||
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
||||||
style: theme.textTheme.labelSmall
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
?.copyWith(color: Colors.grey),
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
resolveUser: notifier.resolveUser,
|
resolveUser: notifier.resolveUser,
|
||||||
|
|
@ -552,16 +532,12 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (memberListOpened.value == true && showMembersByDefault)
|
// if (memberListOpened.value == true && showMembersByDefault) TODO: Member list
|
||||||
MemberList(room.roomData),
|
// MemberList(room),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
endDrawer: showMembersByDefault
|
// endDrawer: showMembersByDefault ? null : MemberList(room),
|
||||||
? null
|
|
||||||
: MemberList(room.roomData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/widgets/form_text_input.dart";
|
import "package:nexus/widgets/form_text_input.dart";
|
||||||
|
|
||||||
class RoomMenu extends StatelessWidget {
|
class RoomMenu extends ConsumerWidget {
|
||||||
final Room room;
|
final Room room;
|
||||||
final IList<Room> children;
|
final IList<Room> children;
|
||||||
const RoomMenu(this.room, {this.children = const IList.empty(), super.key});
|
const RoomMenu(this.room, {this.children = const IList.empty(), super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final danger = Theme.of(context).colorScheme.error;
|
final danger = Theme.of(context).colorScheme.error;
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
void markRead(String roomId) async {
|
|
||||||
// TODO: Set parent read
|
|
||||||
for (final child in children) {
|
|
||||||
// await child.setReadMarker( TODO: Set children read
|
|
||||||
// child.roomData.lastEvent?.eventId,
|
|
||||||
// mRead: child.roomData.lastEvent?.eventId,
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
|
|
@ -33,45 +26,51 @@ class RoomMenu extends StatelessWidget {
|
||||||
// },
|
// },
|
||||||
// child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
|
// child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
|
||||||
// ),
|
// ),
|
||||||
// PopupMenuItem(
|
PopupMenuItem(
|
||||||
// onTap: () => markRead(room.id),
|
onTap: () async {
|
||||||
// child: ListTile(
|
await client.markRead(room);
|
||||||
// leading: Icon(Icons.check),
|
await Future.wait(children.map((child) => client.markRead(child)));
|
||||||
// title: Text("Mark as Read"),
|
},
|
||||||
// ),
|
child: ListTile(
|
||||||
// ),
|
leading: Icon(Icons.check),
|
||||||
// PopupMenuItem(
|
title: Text("Mark as Read"),
|
||||||
// onTap: () => showDialog(
|
),
|
||||||
// context: context,
|
),
|
||||||
// builder: (context) => AlertDialog(
|
PopupMenuItem(
|
||||||
// title: Text("Leave Room"),
|
onTap: () => showDialog(
|
||||||
// content: Text(
|
context: context,
|
||||||
// "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?",
|
builder: (context) => AlertDialog(
|
||||||
// ),
|
title: Text("Leave Room"),
|
||||||
// actions: [
|
content: Text(
|
||||||
// TextButton(
|
"Are you sure you want to leave \"${room.metadata?.name ?? "Unnamed Room"}\"?",
|
||||||
// onPressed: Navigator.of(context).pop,
|
),
|
||||||
// child: Text("Cancel"),
|
actions: [
|
||||||
// ),
|
TextButton(
|
||||||
// TextButton(
|
onPressed: Navigator.of(context).pop,
|
||||||
// onPressed: () async {
|
child: Text("Cancel"),
|
||||||
// Navigator.of(context).pop();
|
),
|
||||||
// final snackbar = ScaffoldMessenger.of(
|
TextButton(
|
||||||
// context,
|
onPressed: () async {
|
||||||
// ).showSnackBar(SnackBar(content: Text("Leaving room...")));
|
Navigator.of(context).pop();
|
||||||
// await room.leave();
|
final snackbar = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
// snackbar.close();
|
SnackBar(
|
||||||
// },
|
content: Text("Leaving room..."),
|
||||||
// child: Text("Leave"),
|
duration: Duration(days: 1),
|
||||||
// ),
|
),
|
||||||
// ],
|
);
|
||||||
// ),
|
await client.leaveRoom(room);
|
||||||
// ),
|
snackbar.close();
|
||||||
// child: ListTile(
|
},
|
||||||
// leading: Icon(Icons.logout, color: danger),
|
child: Text("Leave"),
|
||||||
// title: Text("Leave", style: TextStyle(color: danger)),
|
),
|
||||||
// ),
|
],
|
||||||
// ),
|
),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.logout, color: danger),
|
||||||
|
title: Text("Leave", style: TextStyle(color: danger)),
|
||||||
|
),
|
||||||
|
),
|
||||||
// PopupMenuItem(
|
// PopupMenuItem(
|
||||||
// onTap: () => showDialog(
|
// onTap: () => showDialog(
|
||||||
// context: context,
|
// context: context,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
@ -39,7 +40,7 @@ class Sidebar extends HookConsumerWidget {
|
||||||
final indexOfSelectedRoom = selectedSpace.children.indexWhere(
|
final indexOfSelectedRoom = selectedSpace.children.indexWhere(
|
||||||
(room) => room.metadata?.id == selectedRoomId,
|
(room) => room.metadata?.id == selectedRoomId,
|
||||||
);
|
);
|
||||||
final selectedRoomIndex = indexOfSelected == -1
|
final selectedRoomIndex = indexOfSelectedRoom == -1
|
||||||
? selectedSpace.children.isEmpty
|
? selectedSpace.children.isEmpty
|
||||||
? null
|
? null
|
||||||
: 0
|
: 0
|
||||||
|
|
@ -65,11 +66,17 @@ class Sidebar extends HookConsumerWidget {
|
||||||
fallback: space.icon == null ? null : Icon(space.icon),
|
fallback: space.icon == null ? null : Icon(space.icon),
|
||||||
space.title,
|
space.title,
|
||||||
headers: {}, // TODO
|
headers: {}, // TODO
|
||||||
hasBadge: false,
|
hasBadge:
|
||||||
// space.children.firstWhereOrNull( TODO
|
space.children.firstWhereOrNull(
|
||||||
// (room) => room.roomData.hasNewMessages,
|
(room) => room.metadata?.unreadMessages != 0,
|
||||||
// ) !=
|
) !=
|
||||||
// null,
|
null,
|
||||||
|
badgeNumber: space.children.fold(
|
||||||
|
0,
|
||||||
|
(previousValue, room) =>
|
||||||
|
previousValue +
|
||||||
|
(room.metadata?.unreadNotifications ?? 0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
label: Text(space.title),
|
label: Text(space.title),
|
||||||
padding: EdgeInsets.only(top: 4),
|
padding: EdgeInsets.only(top: 4),
|
||||||
|
|
@ -184,13 +191,16 @@ class Sidebar extends HookConsumerWidget {
|
||||||
// space.client.headers, TODO
|
// space.client.headers, TODO
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
selectedSpace.room?.metadata?.avatar.toString() ??
|
|
||||||
selectedSpace.title,
|
selectedSpace.title,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
actions: [
|
actions: [
|
||||||
if (selectedSpace.room != null) RoomMenu(selectedSpace.room!),
|
if (selectedSpace.room != null)
|
||||||
|
RoomMenu(
|
||||||
|
selectedSpace.room!,
|
||||||
|
children: selectedSpace.children,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: NavigationRail(
|
body: NavigationRail(
|
||||||
|
|
@ -203,8 +213,9 @@ class Sidebar extends HookConsumerWidget {
|
||||||
(room) => NavigationRailDestination(
|
(room) => NavigationRailDestination(
|
||||||
label: Text(room.metadata?.name ?? "Unnamed Room"),
|
label: Text(room.metadata?.name ?? "Unnamed Room"),
|
||||||
icon: AvatarOrHash(
|
icon: AvatarOrHash(
|
||||||
// hasBadge: room.roomData.hasNewMessages, TODO
|
|
||||||
null,
|
null,
|
||||||
|
hasBadge: room.metadata?.unreadMessages != 0,
|
||||||
|
badgeNumber: room.metadata?.unreadNotifications ?? 0,
|
||||||
// room.avatar, TODO
|
// room.avatar, TODO
|
||||||
room.metadata?.name ?? "Unnamed Room",
|
room.metadata?.name ?? "Unnamed Room",
|
||||||
fallback: selectedSpaceId == "dms"
|
fallback: selectedSpaceId == "dms"
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,9 @@ import "package:nexus/widgets/chat_page/html/quoted.dart";
|
||||||
class TopWidget extends ConsumerWidget {
|
class TopWidget extends ConsumerWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
final bool alwaysShow;
|
final bool alwaysShow;
|
||||||
final Map<String, String> headers;
|
|
||||||
final MessageGroupStatus? groupStatus;
|
final MessageGroupStatus? groupStatus;
|
||||||
const TopWidget(
|
const TopWidget(
|
||||||
this.message, {
|
this.message, {
|
||||||
required this.headers,
|
|
||||||
required this.groupStatus,
|
required this.groupStatus,
|
||||||
this.alwaysShow = false,
|
this.alwaysShow = false,
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -62,11 +60,11 @@ class TopWidget extends ConsumerWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Avatar(
|
// Avatar( TODO: images
|
||||||
userId: replyMessage.authorId,
|
// userId: replyMessage.authorId,
|
||||||
headers: headers,
|
// headers: headers,
|
||||||
size: 16,
|
// size: 16,
|
||||||
),
|
// ),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
replyMessage.metadata?["displayName"] ??
|
replyMessage.metadata?["displayName"] ??
|
||||||
|
|
@ -104,7 +102,7 @@ class TopWidget extends ConsumerWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Avatar(userId: message.authorId, headers: headers),
|
// Avatar(userId: message.authorId, headers: headers), TODO: images
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
message.metadata?["displayName"] ?? message.authorId,
|
message.metadata?["displayName"] ?? message.authorId,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue