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