Server selection and login are on different pages. #33
68 changed files with 467 additions and 546 deletions
Merge branch 'main' into better-login
Merge upstream/main into my branch
commit
004c0cd8f4
|
|
@ -110,7 +110,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] Member list
|
- [x] Member list
|
||||||
- [ ] Colors based off of power level
|
- [ ] Colors based off of power level
|
||||||
- [ ] Sort by power level
|
- [ ] Sort by power level
|
||||||
- [ ] Notifications using UnifiedPush
|
- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35))
|
||||||
- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
||||||
- [ ] Invites
|
- [ ] Invites
|
||||||
- [ ] Settings
|
- [ ] Settings
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import "package:nexus/models/account_data.dart";
|
||||||
|
|
||||||
class AccountDataController extends Notifier<IMap<String, AccountData>> {
|
class AccountDataController extends Notifier<IMap<String, AccountData>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, AccountData> build() => const IMap.empty();
|
IMap<String, AccountData> build() => .new();
|
||||||
|
|
||||||
void update(IMap<String, AccountData> newData) =>
|
void update(IMap<String, AccountData> newData) =>
|
||||||
state = IMap({...state.unlock, ...newData.unlock});
|
state = .new({...state.unlock, ...newData.unlock});
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
NotifierProvider<AccountDataController, IMap<String, AccountData>>(
|
NotifierProvider<AccountDataController, IMap<String, AccountData>>(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/user_controller.dart";
|
import "package:nexus/controllers/user_controller.dart";
|
||||||
import "package:nexus/models/configs/user_config.dart";
|
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
|
@ -13,11 +12,11 @@ class AuthorController extends AsyncNotifier<MembershipContent> {
|
||||||
Future<MembershipContent> build() async {
|
Future<MembershipContent> build() async {
|
||||||
final member = await ref.watch(
|
final member = await ref.watch(
|
||||||
UserController.provider(
|
UserController.provider(
|
||||||
UserConfig(roomId: event.roomId, userId: event.sender),
|
.new(roomId: event.roomId, userId: event.sender),
|
||||||
).future,
|
).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
return MembershipContent(
|
return .new(
|
||||||
status: member.status,
|
status: member.status,
|
||||||
avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
|
avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
|
||||||
displayName: event.pmp?.displayName ?? member.displayName,
|
displayName: event.pmp?.displayName ?? member.displayName,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import "package:nexus/controllers/sync_status_controller.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
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/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/models/client_state.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/paginate.dart";
|
import "package:nexus/models/paginate.dart";
|
||||||
import "package:nexus/models/requests/get_event_request.dart";
|
import "package:nexus/models/requests/get_event_request.dart";
|
||||||
|
|
@ -31,7 +30,6 @@ import "package:nexus/models/requests/send_message_request.dart";
|
||||||
import "package:nexus/models/requests/set_membership_request.dart";
|
import "package:nexus/models/requests/set_membership_request.dart";
|
||||||
import "package:nexus/models/room.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/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:path_provider/path_provider.dart";
|
import "package:path_provider/path_provider.dart";
|
||||||
|
|
@ -66,12 +64,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
case "client_state":
|
case "client_state":
|
||||||
ref
|
ref
|
||||||
.watch(ClientStateController.provider.notifier)
|
.watch(ClientStateController.provider.notifier)
|
||||||
.set(ClientState.fromJson(decodedMuksEvent));
|
.set(.fromJson(decodedMuksEvent));
|
||||||
break;
|
break;
|
||||||
case "sync_status":
|
case "sync_status":
|
||||||
ref
|
ref
|
||||||
.watch(SyncStatusController.provider.notifier)
|
.watch(SyncStatusController.provider.notifier)
|
||||||
.set(SyncStatus.fromJson(decodedMuksEvent));
|
.set(.fromJson(decodedMuksEvent));
|
||||||
break;
|
break;
|
||||||
case "init_complete":
|
case "init_complete":
|
||||||
ref.watch(InitCompleteController.provider.notifier).complete();
|
ref.watch(InitCompleteController.provider.notifier).complete();
|
||||||
|
|
@ -81,12 +79,10 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
ref
|
ref
|
||||||
.watch(RoomsController.provider.notifier)
|
.watch(RoomsController.provider.notifier)
|
||||||
.update(
|
.update(
|
||||||
{
|
.new({
|
||||||
event.roomId: Room(
|
event.roomId: .new(events: .new({event.rowId: event})),
|
||||||
events: {event.rowId: event}.toIMap(),
|
}),
|
||||||
),
|
.new(),
|
||||||
}.toIMap(),
|
|
||||||
const ISet.empty(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -210,9 +206,11 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
||||||
final response = await getState(request);
|
final response = await getState(request);
|
||||||
|
|
||||||
return (response ?? await getState(request.copyWith(refetch: true)) ?? [])
|
return .new(
|
||||||
.map((event) => Event.fromJson(event))
|
(response ?? await getState(request.copyWith(refetch: true)) ?? []).map(
|
||||||
.toIList();
|
(event) => .fromJson(event),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<IList<Event>?> getRelatedEvents(
|
Future<IList<Event>?> getRelatedEvents(
|
||||||
|
|
@ -220,20 +218,20 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
) async {
|
) async {
|
||||||
final response =
|
final response =
|
||||||
(await _sendCommand("get_related_events", request.toJson())) as List?;
|
(await _sendCommand("get_related_events", request.toJson())) as List?;
|
||||||
return response?.map((event) => Event.fromJson(event)).toIList();
|
return .new(response?.map((event) => .fromJson(event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Event?> getEvent(GetEventRequest request) async {
|
Future<Event?> getEvent(GetEventRequest request) async {
|
||||||
final json = await _sendCommand("get_event", request.toJson());
|
final json = await _sendCommand("get_event", request.toJson());
|
||||||
return json == null ? null : Event.fromJson(json);
|
return json == null ? null : .fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Paginate> paginate(PaginateRequest request) async =>
|
Future<Paginate> paginate(PaginateRequest request) async =>
|
||||||
Paginate.fromJson(await _sendCommand("paginate", request.toJson()));
|
.fromJson(await _sendCommand("paginate", request.toJson()));
|
||||||
|
|
||||||
Future<Profile> getProfile(String userId) async {
|
Future<Profile> getProfile(String userId) async {
|
||||||
final json = await _sendCommand("get_profile", {"user_id": userId});
|
final json = await _sendCommand("get_profile", {"user_id": userId});
|
||||||
return Profile.fromJsonWithCatch({...json, "id": userId});
|
return .fromJsonWithCatch({...json, "id": userId});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reportEvent(ReportRequest request) =>
|
Future<void> reportEvent(ReportRequest request) =>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ class ClientStateController extends Notifier<ClientState?> {
|
||||||
@override
|
@override
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
void set(ClientState newState) {
|
void set(ClientState newState) => state = newState;
|
||||||
state = newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = NotifierProvider<ClientStateController, ClientState?>(
|
static final provider = NotifierProvider<ClientStateController, ClientState?>(
|
||||||
ClientStateController.new,
|
ClientStateController.new,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
class CrossCacheController extends Notifier<CrossCache> {
|
class CrossCacheController extends Notifier<CrossCache> {
|
||||||
static const String spaceKey = "space";
|
|
||||||
static const String roomKey = "room";
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CrossCache build() => CrossCache();
|
CrossCache build() => .new();
|
||||||
|
|
||||||
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
|
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
|
||||||
CrossCacheController.new,
|
CrossCacheController.new,
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,7 @@ class EmojiController extends AsyncNotifier<EmojiTuple> {
|
||||||
@override
|
@override
|
||||||
Future<EmojiTuple> build() async {
|
Future<EmojiTuple> build() async {
|
||||||
final response = await get(
|
final response = await get(
|
||||||
Uri.https(
|
.https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"),
|
||||||
"github.com",
|
|
||||||
"github/gemoji/raw/refs/heads/master/db/emoji.json",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
|
|
@ -30,19 +27,19 @@ class EmojiController extends AsyncNotifier<EmojiTuple> {
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
||||||
final categoryMap = entries.fold<IMap<String, IList<String>>>(
|
final categoryMap = entries.fold<IMap<String, IList<String>>>(
|
||||||
const IMap.empty(),
|
.new(),
|
||||||
(acc, entry) => acc.update(
|
(acc, entry) => acc.update(
|
||||||
entry.category,
|
entry.category,
|
||||||
(list) => list.add(entry.emoji),
|
(list) => list.add(entry.emoji),
|
||||||
ifAbsent: () => IList([entry.emoji]),
|
ifAbsent: () => .new([entry.emoji]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final keywordMap = entries.fold<IMap<String, IList<String>>>(
|
final keywordMap = entries.fold<IMap<String, IList<String>>>(
|
||||||
const IMap.empty(),
|
.new(),
|
||||||
(acc, entry) => acc.add(
|
(acc, entry) => acc.add(
|
||||||
entry.emoji,
|
entry.emoji,
|
||||||
IList<String>([...entry.tags, ...entry.aliases, entry.description]),
|
.new([...entry.tags, ...entry.aliases, entry.description]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -71,9 +68,9 @@ class EmojiController extends AsyncNotifier<EmojiTuple> {
|
||||||
);
|
);
|
||||||
|
|
||||||
final customKeywords = IMap(
|
final customKeywords = IMap(
|
||||||
Map.fromEntries(
|
.fromEntries(
|
||||||
keywordMap.entries.map(
|
keywordMap.entries.map(
|
||||||
(e) => MapEntry(e.key, e.value.toList(growable: false)),
|
(e) => .new(e.key, e.value.toList(growable: false)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -81,8 +78,7 @@ class EmojiController extends AsyncNotifier<EmojiTuple> {
|
||||||
return (customCategories, customKeywords);
|
return (customCategories, customKeywords);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider = AsyncNotifierProvider<EmojiController, EmojiTuple>(
|
||||||
AsyncNotifierProvider.autoDispose<EmojiController, EmojiTuple>(
|
|
||||||
EmojiController.new,
|
EmojiController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class MembersController extends AsyncNotifier<ISet<Event>> {
|
||||||
RoomsController.provider.select((value) => value[roomId]),
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (room == null) return const ISet.empty();
|
if (room == null) return .new();
|
||||||
|
|
||||||
if (!room.hasFetchedMembers) {
|
if (!room.hasFetchedMembers) {
|
||||||
final fetchedState = await ref
|
final fetchedState = await ref
|
||||||
|
|
@ -38,7 +38,7 @@ class MembersController extends AsyncNotifier<ISet<Event>> {
|
||||||
.map((rowId) => room.events[rowId])
|
.map((rowId) => room.events[rowId])
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toISet() ??
|
.toISet() ??
|
||||||
const ISet.empty();
|
.new();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.autoDispose
|
static final provider = AsyncNotifierProvider.autoDispose
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ class MultiProviderController extends AsyncNotifier<void> {
|
||||||
final IList<AsyncNotifierProvider> providers;
|
final IList<AsyncNotifierProvider> providers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> build() async => await Future.wait(
|
Future<void> build() =>
|
||||||
providers.map((provider) => ref.watch(provider.future)),
|
.wait(providers.map((provider) => ref.watch(provider.future)));
|
||||||
);
|
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider.family<
|
AsyncNotifierProvider.family<
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/models/configs/power_level_config.dart";
|
import "package:nexus/models/configs/power_level_config.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/requests/membership_action.dart";
|
|
||||||
|
|
||||||
class PowerLevelController extends Notifier<bool> {
|
class PowerLevelController extends Notifier<bool> {
|
||||||
final PowerLevelConfig config;
|
final PowerLevelConfig config;
|
||||||
|
|
@ -14,7 +13,7 @@ class PowerLevelController extends Notifier<bool> {
|
||||||
bool build() {
|
bool build() {
|
||||||
if (config case EventPowerLevelConfig(:final eventType)) {
|
if (config case EventPowerLevelConfig(:final eventType)) {
|
||||||
assert(
|
assert(
|
||||||
eventType != EventType.redaction,
|
eventType != .redaction,
|
||||||
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
|
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +28,7 @@ class PowerLevelController extends Notifier<bool> {
|
||||||
final content = event?.content is PowerLevelsContent
|
final content = event?.content is PowerLevelsContent
|
||||||
? event!.content
|
? event!.content
|
||||||
: PowerLevelsContent();
|
: PowerLevelsContent();
|
||||||
|
|
||||||
final user = ref.watch(
|
final user = ref.watch(
|
||||||
ClientStateController.provider.select((value) => value?.userId),
|
ClientStateController.provider.select((value) => value?.userId),
|
||||||
);
|
);
|
||||||
|
|
@ -45,15 +45,15 @@ class PowerLevelController extends Notifier<bool> {
|
||||||
|
|
||||||
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
|
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
|
||||||
switch (action) {
|
switch (action) {
|
||||||
MembershipAction.invite => userLevel >= content.invite,
|
.invite => userLevel >= content.invite,
|
||||||
|
|
||||||
MembershipAction.kick =>
|
.kick =>
|
||||||
userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
|
userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
|
||||||
|
|
||||||
MembershipAction.ban =>
|
.ban =>
|
||||||
userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
|
userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
|
||||||
|
|
||||||
MembershipAction.unban => userLevel >= content.ban,
|
.unban => userLevel >= content.ban,
|
||||||
},
|
},
|
||||||
|
|
||||||
StatePowerLevelConfig(:final eventType) =>
|
StatePowerLevelConfig(:final eventType) =>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/controllers/rooms_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/models/configs/reactions_config.dart";
|
import "package:nexus/models/configs/reactions_config.dart";
|
||||||
import "package:nexus/models/content/reaction.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
import "package:nexus/models/requests/get_related_events_request.dart";
|
|
||||||
|
|
||||||
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
final ReactionsConfig config;
|
final ReactionsConfig config;
|
||||||
|
|
@ -23,7 +22,7 @@ class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
? await ref
|
? await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.getRelatedEvents(
|
.getRelatedEvents(
|
||||||
GetRelatedEventsRequest(
|
.new(
|
||||||
roomId: config.roomId,
|
roomId: config.roomId,
|
||||||
eventId: eventInfo!.$1,
|
eventId: eventInfo!.$1,
|
||||||
relationType: "m.annotation",
|
relationType: "m.annotation",
|
||||||
|
|
@ -33,18 +32,18 @@ class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
|
|
||||||
return reactionEvents
|
return reactionEvents
|
||||||
?.where((event) => event.redactedBy == null)
|
?.where((event) => event.redactedBy == null)
|
||||||
.fold<IMap<String, IList<String>>>(IMap(), (acc, event) {
|
.fold<IMap<String, IList<String>>>(.new(), (acc, event) {
|
||||||
if (event.content case ReactionContent(:final key?)) {
|
if (event.content case ReactionContent(:final key?)) {
|
||||||
return acc.update(
|
return acc.update(
|
||||||
key,
|
key,
|
||||||
(list) => list.add(event.sender),
|
(list) => list.add(event.sender),
|
||||||
ifAbsent: () => IList([event.sender]),
|
ifAbsent: () => .new([event.sender]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}) ??
|
}) ??
|
||||||
const IMap.empty();
|
.new();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,8 @@ import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/reaction.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_related_events_request.dart";
|
|
||||||
import "package:nexus/models/requests/get_room_state_request.dart";
|
|
||||||
import "package:nexus/models/requests/paginate_request.dart";
|
|
||||||
import "package:nexus/models/requests/redact_event_request.dart";
|
import "package:nexus/models/requests/redact_event_request.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/requests/send_event_request.dart";
|
|
||||||
import "package:nexus/models/requests/send_message_request.dart";
|
import "package:nexus/models/requests/send_message_request.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
|
|
@ -28,12 +24,10 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
);
|
);
|
||||||
if (room == null) return const IList.empty();
|
if (room == null) return .new();
|
||||||
|
|
||||||
if (!room.hasFetchedState) {
|
if (!room.hasFetchedState) {
|
||||||
final state = await client.getRoomState(
|
final state = await client.getRoomState(.new(roomId: roomId));
|
||||||
GetRoomStateRequest(roomId: roomId),
|
|
||||||
);
|
|
||||||
|
|
||||||
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
||||||
}
|
}
|
||||||
|
|
@ -44,16 +38,13 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
loadOlder();
|
loadOlder();
|
||||||
}
|
}
|
||||||
|
|
||||||
return IMap<int, int?>.fromValues(
|
return room.timeline
|
||||||
keyMapper: (id) => 9999999 + (id ?? 0),
|
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||||
values: room.sticky,
|
.map((element) => element.value)
|
||||||
)
|
.toIList()
|
||||||
.addAll(room.timeline)
|
.addAll(room.sticky)
|
||||||
.toEntryIList(compare: (a, b) => (b?.key ?? 0).compareTo(a?.key ?? 0))
|
|
||||||
.map((entry) {
|
.map((entry) {
|
||||||
final foundEvent = entry.value == null
|
final foundEvent = entry == null ? null : room.events[entry];
|
||||||
? null
|
|
||||||
: room.events[entry.value!];
|
|
||||||
|
|
||||||
final editedEvent =
|
final editedEvent =
|
||||||
foundEvent == null || foundEvent.lastEditRowId == 0
|
foundEvent == null || foundEvent.lastEditRowId == 0
|
||||||
|
|
@ -86,7 +77,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
final response = await ref
|
final response = await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.paginate(
|
.paginate(
|
||||||
PaginateRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
maxTimelineId: timelineKeys?.isNotEmpty == true
|
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||||
? timelineKeys?.reduce(min)
|
? timelineKeys?.reduce(min)
|
||||||
|
|
@ -112,7 +103,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
const ISet.empty(),
|
.new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.hasMore;
|
return response.hasMore;
|
||||||
|
|
@ -153,20 +144,20 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
text: taggedMessage,
|
text: taggedMessage,
|
||||||
relation: relation == null
|
relation: relation == null
|
||||||
? null
|
? null
|
||||||
: Relation(eventId: relation.eventId, relationType: relationType),
|
: .new(eventId: relation.eventId, relationType: relationType),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(RoomsController.provider.notifier)
|
.watch(RoomsController.provider.notifier)
|
||||||
.update(
|
.update(
|
||||||
{
|
.new({
|
||||||
roomId: Room(
|
roomId: .new(
|
||||||
events: {event.rowId: event}.toIMap(),
|
events: .new({event.rowId: event}),
|
||||||
sticky: {event.rowId}.toISet(),
|
sticky: .new({event.rowId}),
|
||||||
),
|
),
|
||||||
}.toIMap(),
|
}),
|
||||||
const ISet.empty(),
|
.new(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +168,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
) async {
|
) async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final allReactionEvents = await client.getRelatedEvents(
|
final allReactionEvents = await client.getRelatedEvents(
|
||||||
GetRelatedEventsRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
eventId: event.eventId,
|
eventId: event.eventId,
|
||||||
relationType: "m.annotation",
|
relationType: "m.annotation",
|
||||||
|
|
@ -199,9 +190,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
if (reactionEvent != null) {
|
if (reactionEvent != null) {
|
||||||
await ref
|
await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.redactEvent(
|
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId));
|
||||||
RedactEventRequest(eventId: reactionEvent.eventId, roomId: roomId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +198,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
await client.sendEvent(
|
await client.sendEvent(
|
||||||
SendEventRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
type: EventType.reaction.type,
|
type: EventType.reaction.type,
|
||||||
content: {
|
content: {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,29 @@
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/read_receipt.dart";
|
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomsController extends Notifier<IMap<String, Room>> {
|
class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, Room> build() => const IMap.empty();
|
IMap<String, Room> build() => .new();
|
||||||
|
|
||||||
Future<void> addState(
|
Future<void> addState(
|
||||||
String roomId,
|
String roomId,
|
||||||
IList<Event> state, {
|
IList<Event> state, {
|
||||||
bool isMembers = false,
|
bool isMembers = false,
|
||||||
}) async => update(
|
}) async => update(
|
||||||
{
|
.new({
|
||||||
roomId: Room(
|
roomId: Room(
|
||||||
events: IMap.fromEntries(
|
events: .fromEntries(state.map((event) => .new(event.rowId, event))),
|
||||||
state.map((event) => MapEntry(event.rowId, event)),
|
|
||||||
),
|
|
||||||
hasFetchedState: true,
|
hasFetchedState: true,
|
||||||
hasFetchedMembers: isMembers,
|
hasFetchedMembers: isMembers,
|
||||||
state: await Isolate.run(() {
|
state: await Isolate.run(() {
|
||||||
final newState = state.fold(
|
final newState = state.fold<IMap<String, IMap<String, int>>>(
|
||||||
const IMap<String, IMap<String, int>>.empty(),
|
.new(),
|
||||||
(previousValue, stateEvent) => previousValue.add(
|
(previousValue, stateEvent) => previousValue.add(
|
||||||
stateEvent.type,
|
stateEvent.type,
|
||||||
(previousValue[stateEvent.type] ?? const IMap.empty()).add(
|
(previousValue[stateEvent.type] ?? .new()).add(
|
||||||
stateEvent.stateKey!,
|
stateEvent.stateKey!,
|
||||||
stateEvent.rowId,
|
stateEvent.rowId,
|
||||||
),
|
),
|
||||||
|
|
@ -36,8 +32,8 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
return newState;
|
return newState;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}.toIMap(),
|
}),
|
||||||
const ISet.empty(),
|
.new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
|
void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
|
||||||
|
|
@ -65,9 +61,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
existing.state,
|
existing.state,
|
||||||
(previousValue, event) => previousValue.add(
|
(previousValue, event) => previousValue.add(
|
||||||
event.key,
|
event.key,
|
||||||
(previousValue[event.key] ?? const IMap.empty()).addAll(
|
(previousValue[event.key] ?? .new()).addAll(event.value),
|
||||||
event.value,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
reset: false,
|
reset: false,
|
||||||
|
|
@ -82,9 +76,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
existing.receipts,
|
existing.receipts,
|
||||||
(receiptAcc, event) => receiptAcc.add(
|
(receiptAcc, event) => receiptAcc.add(
|
||||||
event.key,
|
event.key,
|
||||||
(receiptAcc[event.key] ?? IList<ReadReceipt>()).addAll(
|
(receiptAcc[event.key] ?? .new()).addAll(event.value),
|
||||||
event.value,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import "package:shared_preferences/shared_preferences.dart";
|
||||||
|
|
||||||
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
|
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
|
||||||
@override
|
@override
|
||||||
Future<SharedPreferences> build() => SharedPreferences.getInstance();
|
Future<SharedPreferences> build() async => .getInstance();
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
|
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "package:nexus/models/space_edge.dart";
|
||||||
|
|
||||||
class SpaceEdgesController extends Notifier<IMap<String, IList<SpaceEdge>>> {
|
class SpaceEdgesController extends Notifier<IMap<String, IList<SpaceEdge>>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, IList<SpaceEdge>> build() => const IMap.empty();
|
IMap<String, IList<SpaceEdge>> build() => .new();
|
||||||
|
|
||||||
void set(IMap<String, IList<SpaceEdge>> newEdges) =>
|
void set(IMap<String, IList<SpaceEdge>> newEdges) =>
|
||||||
state = state.addAll(newEdges);
|
state = state.addAll(newEdges);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
||||||
import "package:nexus/controllers/space_edges_controller.dart";
|
import "package:nexus/controllers/space_edges_controller.dart";
|
||||||
import "package:nexus/models/space.dart";
|
import "package:nexus/models/space.dart";
|
||||||
import "package:nexus/models/room.dart";
|
|
||||||
import "package:nexus/models/space_edge.dart";
|
|
||||||
|
|
||||||
class SpacesController extends Notifier<IList<Space>> {
|
class SpacesController extends Notifier<IList<Space>> {
|
||||||
@override
|
@override
|
||||||
|
|
@ -20,15 +18,15 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
final childRoomsBySpaceId = IMap.fromEntries(
|
final childRoomsBySpaceId = IMap.fromEntries(
|
||||||
topLevelSpaceIds.map((spaceId) {
|
topLevelSpaceIds.map((spaceId) {
|
||||||
ISet<String> walk(String currentId) {
|
ISet<String> walk(String currentId) {
|
||||||
final children = spaceEdges[currentId] ?? IList<SpaceEdge>();
|
final children = spaceEdges[currentId] ?? .new();
|
||||||
|
|
||||||
return children.fold<ISet<String>>(const ISet.empty(), (acc, edge) {
|
return children.fold<ISet<String>>(.new(), (acc, edge) {
|
||||||
final childId = edge.childId;
|
final childId = edge.childId;
|
||||||
final isSpace = spaceEdges.containsKey(childId);
|
final isSpace = spaceEdges.containsKey(childId);
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
.addAll(!isSpace ? ISet([childId]) : const ISet.empty())
|
.addAll(!isSpace ? ISet([childId]) : const .empty())
|
||||||
.addAll(isSpace ? walk(childId) : const ISet.empty());
|
.addAll(isSpace ? walk(childId) : const .empty());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +86,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 = childRoomsBySpaceId[id] ?? IList<Room>();
|
final children = childRoomsBySpaceId[id] ?? .new();
|
||||||
return Space(
|
return Space(
|
||||||
id: id,
|
id: id,
|
||||||
title: room.metadata?.name ?? "Unnamed Room",
|
title: room.metadata?.name ?? "Unnamed Room",
|
||||||
|
|
@ -100,13 +98,13 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
||||||
return <Space>[
|
return <Space>[
|
||||||
Space(
|
.new(
|
||||||
id: "home",
|
id: "home",
|
||||||
title: "Home",
|
title: "Home",
|
||||||
icon: Icons.home,
|
icon: Icons.home,
|
||||||
children: homeRooms,
|
children: homeRooms,
|
||||||
),
|
),
|
||||||
Space(
|
.new(
|
||||||
id: "dms",
|
id: "dms",
|
||||||
title: "Direct Messages",
|
title: "Direct Messages",
|
||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
|
|
@ -116,7 +114,8 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
(space) => space.copyWith(
|
(space) => space.copyWith(
|
||||||
children: space.children
|
children: .new(
|
||||||
|
space.children
|
||||||
.sortedBy(
|
.sortedBy(
|
||||||
(element) =>
|
(element) =>
|
||||||
element
|
element
|
||||||
|
|
@ -126,8 +125,8 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
.sortedBy((room) => room.metadata?.unreadMessages ?? 0)
|
.sortedBy((room) => room.metadata?.unreadMessages ?? 0)
|
||||||
.reversed
|
.reversed,
|
||||||
.toIList(),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class SyncStatusController extends Notifier<SyncStatus?> {
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
void set(SyncStatus newStatus) {
|
void set(SyncStatus newStatus) {
|
||||||
if (newStatus.type == SyncStatusType.permanentlyFailed) {
|
if (newStatus.type == .permanentlyFailed) {
|
||||||
showError(newStatus.error ?? "Syncing failed");
|
showError(newStatus.error ?? "Syncing failed");
|
||||||
}
|
}
|
||||||
state = newStatus;
|
state = newStatus;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
class TopLevelSpacesController extends Notifier<IList<String>> {
|
class TopLevelSpacesController extends Notifier<IList<String>> {
|
||||||
@override
|
@override
|
||||||
IList<String> build() => const IList.empty();
|
IList<String> build() => .new();
|
||||||
|
|
||||||
void set(IList<String> newSpaces) => state = newSpaces;
|
void set(IList<String> newSpaces) => state = newSpaces;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
if (homeserver != null && !link.contains("matrix.to")) {
|
if (homeserver != null && !link.contains("matrix.to")) {
|
||||||
{
|
{
|
||||||
final response = await get(
|
final response = await get(
|
||||||
Uri.parse(homeserver)
|
.parse(homeserver)
|
||||||
.resolve("/_matrix/client/v1/media/preview_url")
|
.resolve("/_matrix/client/v1/media/preview_url")
|
||||||
.replace(queryParameters: {"url": link}),
|
.replace(queryParameters: {"url": link}),
|
||||||
headers: await ref.watch(HeaderController.provider.future),
|
headers: await ref.watch(HeaderController.provider.future),
|
||||||
|
|
@ -34,7 +34,7 @@ class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
? null
|
? null
|
||||||
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
||||||
|
|
||||||
return OpenGraphData.fromJson(decodedValue).copyWith(imageUrl: image);
|
return .fromJson(decodedValue).copyWith(imageUrl: image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import "package:nexus/controllers/profile_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/models/configs/user_config.dart";
|
import "package:nexus/models/configs/user_config.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
|
|
||||||
class UserController extends AsyncNotifier<MembershipContent> {
|
class UserController extends AsyncNotifier<MembershipContent> {
|
||||||
final UserConfig config;
|
final UserConfig config;
|
||||||
|
|
@ -31,8 +30,9 @@ class UserController extends AsyncNotifier<MembershipContent> {
|
||||||
final profile = await ref.watch(
|
final profile = await ref.watch(
|
||||||
ProfileController.provider(config.userId).future,
|
ProfileController.provider(config.userId).future,
|
||||||
);
|
);
|
||||||
return MembershipContent(
|
|
||||||
status: MembershipStatus.leave,
|
return .new(
|
||||||
|
status: .leave,
|
||||||
avatarUrl: profile.avatarUrl,
|
avatarUrl: profile.avatarUrl,
|
||||||
displayName: profile.displayName ?? config.userId.localpart,
|
displayName: profile.displayName ?? config.userId.localpart,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class ViaController extends Notifier<String> {
|
class ViaController extends Notifier<String> {
|
||||||
|
|
@ -45,7 +44,7 @@ class ViaController extends Notifier<String> {
|
||||||
: room.events[membershipEventId];
|
: room.events[membershipEventId];
|
||||||
|
|
||||||
if (member?.content case MembershipContent(:final status)) {
|
if (member?.content case MembershipContent(:final status)) {
|
||||||
if (status == MembershipStatus.join) {
|
if (status == .join) {
|
||||||
addUserId(member?.stateKey);
|
addUserId(member?.stateKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
extension GomuksOwnedBufferToX on GomuksOwnedBuffer {
|
extension GomuksOwnedBufferToX on GomuksOwnedBuffer {
|
||||||
Uint8List toBytes() {
|
Uint8List toBytes() {
|
||||||
try {
|
try {
|
||||||
if (base == nullptr || length <= 0) return Uint8List(0);
|
if (base == nullptr || length <= 0) return .new(0);
|
||||||
return Uint8List.fromList(base.asTypedList(length));
|
return .fromList(base.asTypedList(length));
|
||||||
} finally {
|
} finally {
|
||||||
calloc.free(base);
|
calloc.free(base);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extension MxcToHttps on Uri {
|
extension MxcToHttps on Uri {
|
||||||
Uri mxcToHttps(String homeserver) => Uri.parse(
|
Uri mxcToHttps(String homeserver) =>
|
||||||
homeserver,
|
.parse(homeserver).resolve("_matrix/client/v1/media/download/$host$path");
|
||||||
).resolve("_matrix/client/v1/media/download/$host$path");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
extension SchemeToTheme on ColorScheme {
|
extension SchemeToTheme on ColorScheme {
|
||||||
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
ThemeData get theme => .from(colorScheme: this).copyWith(
|
||||||
cardTheme: CardThemeData(color: primaryContainer),
|
cardTheme: .new(color: primaryContainer),
|
||||||
|
popupMenuTheme: .new(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: .circular(16)),
|
||||||
|
color: surfaceContainerHigh,
|
||||||
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
backgroundColor: surfaceContainerLow,
|
backgroundColor: surfaceContainerLow,
|
||||||
),
|
),
|
||||||
popupMenuTheme: PopupMenuThemeData(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
||||||
color: surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
textTheme: ThemeData(
|
textTheme: ThemeData(
|
||||||
fontFamilyFallback: ["sans", "emoji"],
|
fontFamilyFallback: ["sans", "emoji"],
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ extension ShowContextMenu on BuildContext {
|
||||||
|
|
||||||
showMenu(
|
showMenu(
|
||||||
context: this,
|
context: this,
|
||||||
constraints: BoxConstraints.loose(Size.infinite),
|
constraints: .loose(Size.infinite),
|
||||||
position: RelativeRect.fromLTRB(
|
position: .fromLTRB(
|
||||||
globalPosition.dx,
|
globalPosition.dx,
|
||||||
globalPosition.dy,
|
globalPosition.dy,
|
||||||
overlay.size.width - globalPosition.dx,
|
overlay.size.width - globalPosition.dx,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ extension ShowUserPopover on BuildContext {
|
||||||
children: [
|
children: [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: false,
|
enabled: false,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: .symmetric(horizontal: 16, vertical: 8),
|
||||||
child: IconTheme(
|
child: IconTheme(
|
||||||
data: IconThemeData(),
|
data: .new(),
|
||||||
child: UserPopover(member, userId, roomId: roomId),
|
child: UserPopover(member, userId, roomId: roomId),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
extension SizeToString on int {
|
extension SizeToString on int {
|
||||||
String get sizeAsString {
|
String get sizeAsString {
|
||||||
const IListConst<String> suffixes = IListConst([
|
const suffixes = IListConst(["B", "KB", "MB", "GB", "TB", "PB"]);
|
||||||
"B",
|
|
||||||
"KB",
|
|
||||||
"MB",
|
|
||||||
"GB",
|
|
||||||
"TB",
|
|
||||||
"PB",
|
|
||||||
]);
|
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
var size = toDouble();
|
var size = toDouble();
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ class LaunchHelper {
|
||||||
try {
|
try {
|
||||||
return await ul.launchUrl(
|
return await ul.launchUrl(
|
||||||
url,
|
url,
|
||||||
mode: useWebview
|
mode: useWebview ? .inAppBrowserView : .externalApplication,
|
||||||
? ul.LaunchMode.inAppBrowserView
|
|
||||||
: ul.LaunchMode.externalApplication,
|
|
||||||
);
|
);
|
||||||
} on PlatformException catch (_) {
|
} on PlatformException catch (_) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class Content {
|
||||||
?.contentFromJson ??
|
?.contentFromJson ??
|
||||||
Content.fromJson)(json);
|
Content.fromJson)(json);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error is Error) return Content(parseError: error);
|
if (error is Error) return .new(parseError: error);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/emoji_controller.dart";
|
||||||
import "package:nexus/controllers/init_complete_controller.dart";
|
import "package:nexus/controllers/init_complete_controller.dart";
|
||||||
import "package:nexus/controllers/key_controller.dart";
|
import "package:nexus/controllers/key_controller.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
|
|
@ -17,6 +18,7 @@ class ChatPage extends ConsumerWidget {
|
||||||
final showMembersByDefault = constraints.maxWidth > 1000;
|
final showMembersByDefault = constraints.maxWidth > 1000;
|
||||||
final initComplete = ref.watch(InitCompleteController.provider);
|
final initComplete = ref.watch(InitCompleteController.provider);
|
||||||
final roomId = ref.watch(KeyController.provider(KeyController.roomKey));
|
final roomId = ref.watch(KeyController.provider(KeyController.roomKey));
|
||||||
|
ref.read(EmojiController.provider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: initComplete ? null : Appbar(),
|
appBar: initComplete ? null : Appbar(),
|
||||||
|
|
@ -35,7 +37,7 @@ class ChatPage extends ConsumerWidget {
|
||||||
)
|
)
|
||||||
: Center(
|
: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
children: [Loading(), Text("Syncing...")],
|
children: [Loading(), Text("Syncing...")],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ class VerifyPage extends HookConsumerWidget {
|
||||||
body: AlertDialog(
|
body: AlertDialog(
|
||||||
title: Text("Verify"),
|
title: Text("Verify"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
|
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
|
||||||
|
|
@ -41,11 +41,11 @@ class VerifyPage extends HookConsumerWidget {
|
||||||
: () async {
|
: () async {
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final snackbar = scaffoldMessenger.showSnackBar(
|
final snackbar = scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
content: Text(
|
content: Text(
|
||||||
"Attempting to verify with recovery key...",
|
"Attempting to verify with recovery key...",
|
||||||
),
|
),
|
||||||
duration: Duration(days: 999),
|
duration: .new(days: 999),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -60,13 +60,13 @@ class VerifyPage extends HookConsumerWidget {
|
||||||
isVerifying.value = false;
|
isVerifying.value = false;
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.errorContainer,
|
).colorScheme.errorContainer,
|
||||||
content: Text(
|
content: Text(
|
||||||
"Verification failed. Is your passphrase correct?\nError: $error",
|
"Verification failed. Is your passphrase correct?\nError: $error",
|
||||||
style: TextStyle(
|
style: .new(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onErrorContainer,
|
).colorScheme.onErrorContainer,
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.scrolledUnderElevation,
|
this.scrolledUnderElevation,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.actions = const IList.empty(),
|
this.actions = const .empty(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const .fromHeight(kToolbarHeight);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -42,7 +42,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
leading: InkWell(onTap: onTap, child: leading),
|
leading: InkWell(onTap: onTap, child: leading),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
scrolledUnderElevation: scrolledUnderElevation,
|
scrolledUnderElevation: scrolledUnderElevation,
|
||||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
|
actionsPadding: const .symmetric(horizontal: 8),
|
||||||
title: InkWell(
|
title: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: IgnorePointer(child: title),
|
child: IgnorePointer(child: title),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class AvatarOrHash extends ConsumerWidget {
|
||||||
smallSize: 12,
|
smallSize: 12,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular((height - 8) / 2.5)),
|
borderRadius: .all(.circular((height - 8) / 2.5)),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: height,
|
width: height,
|
||||||
height: height,
|
height: height,
|
||||||
|
|
@ -60,10 +60,10 @@ class AvatarOrHash extends ConsumerWidget {
|
||||||
ref.watch(CrossCacheController.provider),
|
ref.watch(CrossCacheController.provider),
|
||||||
headers: ref.headers,
|
headers: ref.headers,
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: .cover,
|
||||||
loadingBuilder: (_, child, loadingProgress) =>
|
loadingBuilder: (_, child, loadingProgress) =>
|
||||||
loadingProgress == null ? child : box,
|
loadingProgress == null ? child : fallback ?? box,
|
||||||
errorBuilder: (_, _, _) => box,
|
errorBuilder: (_, _, _) => fallback ?? box,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ 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:nexus/controllers/power_level_controller.dart";
|
import "package:nexus/controllers/power_level_controller.dart";
|
||||||
import "package:nexus/models/configs/power_level_config.dart";
|
|
||||||
import "package:nexus/models/content/content.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/mention_overlay.dart";
|
import "package:nexus/widgets/composer/mention_overlay.dart";
|
||||||
|
|
@ -42,7 +40,7 @@ class Composer extends HookConsumerWidget {
|
||||||
final shouldMention = useState(true);
|
final shouldMention = useState(true);
|
||||||
final query = useState("");
|
final query = useState("");
|
||||||
|
|
||||||
if (relationType == RelationType.edit && controller.value.text.isEmpty) {
|
if (relationType == .edit && controller.value.text.isEmpty) {
|
||||||
controller.value.text = relatedEvent?.localContent?.editSource ?? "";
|
controller.value.text = relatedEvent?.localContent?.editSource ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +49,7 @@ class Composer extends HookConsumerWidget {
|
||||||
onSend(
|
onSend(
|
||||||
controller.value.formattedText,
|
controller.value.formattedText,
|
||||||
shouldMention: shouldMention.value,
|
shouldMention: shouldMention.value,
|
||||||
tags: controller.value.tags.toIList(),
|
tags: .new(controller.value.tags),
|
||||||
);
|
);
|
||||||
|
|
||||||
onDismiss();
|
onDismiss();
|
||||||
|
|
@ -60,13 +58,13 @@ class Composer extends HookConsumerWidget {
|
||||||
|
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: .bold,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsetsGeometry.all(12),
|
padding: .all(12),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: .all(.circular(12)),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
RelationPreview(
|
RelationPreview(
|
||||||
|
|
@ -79,17 +77,14 @@ class Composer extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: .symmetric(horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: .center,
|
||||||
children:
|
children:
|
||||||
ref.watch(
|
ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig(
|
.new(eventType: .message, roomId: roomId),
|
||||||
eventType: EventType.message,
|
|
||||||
roomId: roomId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
? [
|
? [
|
||||||
|
|
@ -124,7 +119,7 @@ class Composer extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlutterTagger(
|
child: FlutterTagger(
|
||||||
triggerStrategy: TriggerStrategy.eager,
|
triggerStrategy: .eager,
|
||||||
overlay: MentionOverlay(
|
overlay: MentionOverlay(
|
||||||
roomId,
|
roomId,
|
||||||
query: query.value,
|
query: query.value,
|
||||||
|
|
@ -144,16 +139,16 @@ class Composer extends HookConsumerWidget {
|
||||||
maxLines: 12,
|
maxLines: 12,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: .new(
|
||||||
hintText: "Your message here...",
|
hintText: "Your message here...",
|
||||||
border: InputBorder.none,
|
border: .none,
|
||||||
),
|
),
|
||||||
controller: controller.value,
|
controller: controller.value,
|
||||||
key: key,
|
key: key,
|
||||||
onSubmitted: (_) => send(),
|
onSubmitted: (_) => send(),
|
||||||
// Don't defocus on submit
|
// Don't defocus on submit
|
||||||
onEditingComplete: () {},
|
onEditingComplete: () {},
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: .done,
|
||||||
focusNode: node,
|
focusNode: node,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -167,10 +162,7 @@ class Composer extends HookConsumerWidget {
|
||||||
: [
|
: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsGeometry.symmetric(
|
padding: .symmetric(horizontal: 8, vertical: 12),
|
||||||
horizontal: 8,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
"You don't have permission to send messages in this room...",
|
"You don't have permission to send messages in this room...",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/via_controller.dart";
|
import "package:nexus/controllers/via_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/models/configs/members_by_status_config.dart";
|
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
||||||
|
|
@ -29,21 +27,18 @@ class MentionOverlay extends ConsumerWidget {
|
||||||
final rooms = ref.watch(RoomsController.provider);
|
final rooms = ref.watch(RoomsController.provider);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(8),
|
padding: .all(8),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: .all(.circular(12)),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
padding: EdgeInsets.all(8),
|
padding: .all(8),
|
||||||
child: switch (triggerCharacter) {
|
child: switch (triggerCharacter) {
|
||||||
"@" =>
|
"@" =>
|
||||||
ref
|
ref
|
||||||
.watch(
|
.watch(
|
||||||
MembersByStatusController.provider(
|
MembersByStatusController.provider(
|
||||||
MembersByStatusConfig(
|
.new(roomId: roomId, status: .join),
|
||||||
roomId: roomId,
|
|
||||||
status: MembershipStatus.join,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
|
|
|
||||||
|
|
@ -27,38 +27,34 @@ class RelationPreview extends ConsumerWidget {
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
padding: .symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (relationType == RelationType.edit)
|
if (relationType == .edit)
|
||||||
Text(
|
Text("Editing message:", style: .new(fontWeight: .bold)),
|
||||||
"Editing message:",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: .symmetric(vertical: 8),
|
||||||
child: EventPreview(relatedEvent!),
|
child: EventPreview(relatedEvent!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (relationType == RelationType.reply)
|
if (relationType == .reply)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: toggleShouldMention,
|
onPressed: toggleShouldMention,
|
||||||
child: Text(
|
child: Text(
|
||||||
shouldMention ? "@On" : "@Off",
|
shouldMention ? "@On" : "@Off",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: .w900,
|
||||||
color: shouldMention ? null : Theme.of(context).disabledColor,
|
color: shouldMention ? null : Theme.of(context).disabledColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip:
|
tooltip: "Cancel ${relationType == .edit ? "edit" : "reply"}",
|
||||||
"Cancel ${relationType == RelationType.edit ? "edit" : "reply"}",
|
|
||||||
onPressed: onDismiss,
|
onPressed: onDismiss,
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ class DividerText extends StatelessWidget {
|
||||||
child: Divider(color: Theme.of(context).colorScheme.onSurface),
|
child: Divider(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: constraints.maxWidth - 32),
|
constraints: .new(maxWidth: constraints.maxWidth - 32),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const .all(8),
|
||||||
child: Text(text, style: Theme.of(context).textTheme.labelLarge),
|
child: Text(text, style: Theme.of(context).textTheme.labelLarge),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@ class EmojiPickerButton extends HookConsumerWidget {
|
||||||
Widget build(_, WidgetRef ref) => IconButton(
|
Widget build(_, WidgetRef ref) => IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
onPressed?.call();
|
onPressed?.call();
|
||||||
final controller = this.controller ?? TextEditingController();
|
final controller = this.controller ?? .new();
|
||||||
|
|
||||||
final emojis = await ref.watch(EmojiController.provider.future);
|
final emojis = await ref.watch(EmojiController.provider.future);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => EmojiKeyboardView(
|
builder: (context) => EmojiKeyboardView(
|
||||||
config: EmojiViewConfig(
|
config: .new(
|
||||||
showRecentTab: false,
|
showRecentTab: false,
|
||||||
customCategories: emojis.$1.unlock,
|
customCategories: emojis.$1.unlock,
|
||||||
customKeywords: emojis.$2.unlock,
|
customKeywords: emojis.$2.unlock,
|
||||||
|
|
@ -37,7 +37,8 @@ class EmojiPickerButton extends HookConsumerWidget {
|
||||||
textController: controller
|
textController: controller
|
||||||
..addListener(() {
|
..addListener(() {
|
||||||
// Without this, there will sometimes be a debugLocked is not true error sometimes
|
// Without this, there will sometimes be a debugLocked is not true error sometimes
|
||||||
Future.delayed(Duration.zero, () {
|
// It might be preferable to use a microtask instead of a `Future.delayed`.
|
||||||
|
Future.delayed(.zero, () {
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
onSelection?.call(controller.text);
|
onSelection?.call(controller.text);
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ class EventPreview extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => IgnorePointer(
|
Widget build(BuildContext context) => IgnorePointer(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
padding: .symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
if (event.content is MessageContent) MessageAvatar(event),
|
if (event.content is MessageContent) MessageAvatar(event),
|
||||||
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: .center,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 2,
|
runSpacing: 2,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,12 @@ class ExpandableImage extends ConsumerWidget {
|
||||||
builder: (_) => LayoutBuilder(
|
builder: (_) => LayoutBuilder(
|
||||||
builder: (context, constraints) => Dialog(
|
builder: (context, constraints) => Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
insetPadding: EdgeInsets.all(constraints.maxWidth / 100),
|
insetPadding: .all(constraints.maxWidth / 100),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: .new(minWidth: min(constraints.maxWidth, 1000)),
|
||||||
minWidth: min(constraints.maxWidth, 1000),
|
|
||||||
),
|
|
||||||
child: InteractiveViewer(
|
child: InteractiveViewer(
|
||||||
child: Image(
|
child: Image(
|
||||||
fit: BoxFit.contain,
|
fit: .contain,
|
||||||
errorBuilder: (_, error, stackTrace) => ErrorDialog(
|
errorBuilder: (_, error, stackTrace) => ErrorDialog(
|
||||||
"Loading failed for $source\nError: $error",
|
"Loading failed for $source\nError: $error",
|
||||||
stackTrace,
|
stackTrace,
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,7 @@ class FileCard extends StatelessWidget {
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Icons.file_copy),
|
leading: Icon(Icons.file_copy),
|
||||||
title: Text(
|
title: Text(filename ?? "file", maxLines: 1, overflow: .ellipsis),
|
||||||
filename ?? "file",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: info?.size == null ? null : Text(info!.size!.sizeAsString),
|
subtitle: info?.size == null ? null : Text(info!.size!.sizeAsString),
|
||||||
// TODO: Downloading files
|
// TODO: Downloading files
|
||||||
trailing: IconButton(onPressed: null, icon: Icon(Icons.download)),
|
trailing: IconButton(onPressed: null, icon: Icon(Icons.download)),
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ class FlashWrapper extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ClipRRect(
|
Widget build(BuildContext context) => ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: .all(.circular(12)),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
padding: isFlashing ? EdgeInsets.all(8) : EdgeInsets.all(0),
|
padding: isFlashing ? .all(8) : .all(0),
|
||||||
color: isFlashing
|
color: isFlashing
|
||||||
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
duration: Duration(milliseconds: 250),
|
duration: .new(milliseconds: 250),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,12 @@ class FormTextInput extends StatelessWidget {
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
inputFormatters: formatters,
|
inputFormatters: formatters,
|
||||||
textCapitalization: capitalize
|
textCapitalization: capitalize ? .sentences : .none,
|
||||||
? TextCapitalization.sentences
|
|
||||||
: TextCapitalization.none,
|
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
autocorrect: autocorrect,
|
autocorrect: autocorrect,
|
||||||
obscureText: obscure,
|
obscureText: obscure,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
decoration: InputDecoration(
|
decoration: .new(
|
||||||
labelText: title,
|
labelText: title,
|
||||||
border: border ?? (outlined ? null : const UnderlineInputBorder()),
|
border: border ?? (outlined ? null : const UnderlineInputBorder()),
|
||||||
suffixIcon: trailing,
|
suffixIcon: trailing,
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,20 @@ class CodeBlock extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: .all(.circular(16)),
|
||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
child: IntrinsicWidth(
|
child: IntrinsicWidth(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: .spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
padding: .symmetric(horizontal: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
lang.substring(0, min(lang.length, 15)),
|
lang.substring(0, min(lang.length, 15)),
|
||||||
style: TextStyle(fontFamily: "monospace"),
|
style: .new(fontFamily: "monospace"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
|
|
@ -37,13 +37,13 @@ class CodeBlock extends StatelessWidget {
|
||||||
ColoredBox(
|
ColoredBox(
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(minWidth: 250),
|
constraints: .new(minWidth: 250),
|
||||||
padding: EdgeInsets.all(8),
|
padding: .all(8),
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
code,
|
code,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 99,
|
maxLines: 99,
|
||||||
style: TextStyle(fontFamily: "monospace"),
|
style: .new(fontFamily: "monospace"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,7 @@ class Html extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
errorBuilder: (_, error, _) => Text(
|
errorBuilder: (_, error, _) => Text(
|
||||||
"Image Failed to Load",
|
"Image Failed to Load",
|
||||||
style: TextStyle(
|
style: .new(color: Theme.of(context).colorScheme.error),
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
height: height.toDouble(),
|
height: height.toDouble(),
|
||||||
width: width?.toDouble(),
|
width: width?.toDouble(),
|
||||||
|
|
@ -146,15 +144,14 @@ class Html extends ConsumerWidget {
|
||||||
element.attributes
|
element.attributes
|
||||||
.mapTo<MapEntry<String, String>?>(
|
.mapTo<MapEntry<String, String>?>(
|
||||||
(key, value) => switch (key) {
|
(key, value) => switch (key) {
|
||||||
"data-mx-color" => MapEntry("color", value),
|
"data-mx-color" => .new("color", value),
|
||||||
"data-mx-bg-color" => MapEntry("background-color", value),
|
"data-mx-bg-color" => .new("background-color", value),
|
||||||
_ => null,
|
_ => null,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.nonNulls,
|
.nonNulls,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
onTapUrl: (url) =>
|
onTapUrl: (url) => ref.watch(LaunchHelper.provider).launchUrl(.parse(url)),
|
||||||
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/user_controller.dart";
|
import "package:nexus/controllers/user_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
import "package:nexus/models/configs/user_config.dart";
|
|
||||||
|
|
||||||
class MentionChip extends ConsumerWidget {
|
class MentionChip extends ConsumerWidget {
|
||||||
final String? roomId;
|
final String? roomId;
|
||||||
|
|
@ -16,9 +15,7 @@ class MentionChip extends ConsumerWidget {
|
||||||
final membership = mention?.startsWith("@") == true
|
final membership = mention?.startsWith("@") == true
|
||||||
? ref
|
? ref
|
||||||
.watch(
|
.watch(
|
||||||
UserController.provider(
|
UserController.provider(.new(roomId: roomId, userId: mention!)),
|
||||||
UserConfig(roomId: roomId, userId: mention!),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.whenOrNull(data: (data) => data)
|
.whenOrNull(data: (data) => data)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -41,8 +38,8 @@ class MentionChip extends ConsumerWidget {
|
||||||
label: Text(
|
label: Text(
|
||||||
(membership == null ? null : "@${membership.displayName}") ??
|
(membership == null ? null : "@${membership.displayName}") ??
|
||||||
mention,
|
mention,
|
||||||
style: TextStyle(
|
style: .new(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: .bold,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ class Quoted extends StatelessWidget {
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
left: BorderSide(width: 4, color: Theme.of(context).dividerColor),
|
left: .new(width: 4, color: Theme.of(context).dividerColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(padding: EdgeInsets.only(left: 8), child: child),
|
child: Padding(padding: .only(left: 8), child: child),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,15 @@ class SpoilerText extends HookWidget {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => revealed.value = !revealed.value,
|
onTap: () => revealed.value = !revealed.value,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const .new(milliseconds: 100),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
padding: const .symmetric(horizontal: 4, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: revealed.value ? Colors.transparent : Colors.blueGrey,
|
color: revealed.value ? Colors.transparent : Colors.blueGrey,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: .circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(color: revealed.value ? null : Colors.transparent),
|
style: .new(color: revealed.value ? null : Colors.transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.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";
|
||||||
|
|
@ -7,7 +6,6 @@ import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/controllers/key_controller.dart";
|
import "package:nexus/controllers/key_controller.dart";
|
||||||
import "package:nexus/controllers/spaces_controller.dart";
|
import "package:nexus/controllers/spaces_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
import "package:nexus/models/requests/join_room_request.dart";
|
|
||||||
import "package:nexus/widgets/form_text_input.dart";
|
import "package:nexus/widgets/form_text_input.dart";
|
||||||
|
|
||||||
class JoinDialog extends HookWidget {
|
class JoinDialog extends HookWidget {
|
||||||
|
|
@ -20,8 +18,8 @@ class JoinDialog extends HookWidget {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("Join a Room"),
|
title: Text("Join a Room"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text("Enter the room alias, Matrix URI, or Matrix.to link."),
|
Text("Enter the room alias, Matrix URI, or Matrix.to link."),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
|
@ -45,7 +43,7 @@ class JoinDialog extends HookWidget {
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
final snackbar = scaffoldMessenger.showSnackBar(
|
final snackbar = scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
content: Text("Joining room $roomIdOrAlias."),
|
content: Text("Joining room $roomIdOrAlias."),
|
||||||
duration: Duration(days: 999),
|
duration: Duration(days: 999),
|
||||||
),
|
),
|
||||||
|
|
@ -55,9 +53,9 @@ class JoinDialog extends HookWidget {
|
||||||
final id = await ref
|
final id = await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.joinRoom(
|
.joinRoom(
|
||||||
JoinRoomRequest(
|
.new(
|
||||||
roomIdOrAlias: roomIdOrAlias,
|
roomIdOrAlias: roomIdOrAlias,
|
||||||
via: IList(
|
via: .new(
|
||||||
Uri.tryParse(
|
Uri.tryParse(
|
||||||
roomAlias.text.replaceAll("/#", ""),
|
roomAlias.text.replaceAll("/#", ""),
|
||||||
)?.queryParametersAll["via"] ??
|
)?.queryParametersAll["via"] ??
|
||||||
|
|
@ -69,9 +67,9 @@ class JoinDialog extends HookWidget {
|
||||||
snackbar.close();
|
snackbar.close();
|
||||||
|
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
content: Text("Room $roomIdOrAlias successfully joined."),
|
content: Text("Room $roomIdOrAlias successfully joined."),
|
||||||
action: SnackBarAction(
|
action: .new(
|
||||||
label: "Open",
|
label: "Open",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final spaces = ref.watch(SpacesController.provider);
|
final spaces = ref.watch(SpacesController.provider);
|
||||||
|
|
@ -113,13 +111,13 @@ class JoinDialog extends HookWidget {
|
||||||
snackbar.close();
|
snackbar.close();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.errorContainer,
|
).colorScheme.errorContainer,
|
||||||
content: Text(
|
content: Text(
|
||||||
error.toString(),
|
error.toString(),
|
||||||
style: TextStyle(
|
style: .new(
|
||||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ class MessageDisplayname extends ConsumerWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) =>
|
Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch(
|
||||||
switch (ref.watch(AuthorController.provider(event))) {
|
AuthorController.provider(event),
|
||||||
|
)) {
|
||||||
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
||||||
onTapUp: clickable
|
onTapUp: clickable
|
||||||
? (details) => context.showUserPopover(
|
? (details) => context.showUserPopover(
|
||||||
|
|
@ -31,18 +32,14 @@ class MessageDisplayname extends ConsumerWidget {
|
||||||
: null,
|
: null,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: .center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value.displayName ?? event.sender.localpart,
|
value.displayName ?? event.sender.localpart,
|
||||||
style:
|
style:
|
||||||
style ??
|
style ?? .new(color: event.sender.colorHash, fontWeight: .bold),
|
||||||
TextStyle(
|
|
||||||
color: event.sender.colorHash,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (event.pmp != null)
|
if (event.pmp != null)
|
||||||
|
|
@ -50,20 +47,17 @@ class MessageDisplayname extends ConsumerWidget {
|
||||||
"(via ${event.sender})",
|
"(via ${event.sender})",
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: event.sender.colorHash,
|
color: event.sender.colorHash,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: .bold,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => Text(
|
_ => Text(
|
||||||
event.sender.localpart,
|
event.sender.localpart,
|
||||||
style: TextStyle(
|
style: .new(color: event.sender.colorHash, fontWeight: .bold),
|
||||||
color: event.sender.colorHash,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ class LinkifiedText extends ConsumerWidget {
|
||||||
text: text,
|
text: text,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: style,
|
style: style,
|
||||||
options: LinkifyOptions(humanize: false),
|
options: .new(humanize: false),
|
||||||
onOpen: (link) =>
|
onOpen: (link) =>
|
||||||
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(link.url)),
|
ref.watch(LaunchHelper.provider).launchUrl(.parse(link.url)),
|
||||||
linkStyle: TextStyle(color: Theme.of(context).colorScheme.primary),
|
linkStyle: .new(color: Theme.of(context).colorScheme.primary),
|
||||||
overflow: maxLines == null ? null : TextOverflow.ellipsis,
|
overflow: maxLines == null ? null : .ellipsis,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class Loading extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Center(
|
Widget build(BuildContext context) => Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: .all(16),
|
||||||
child: SizedBox(height: height, child: CircularProgressIndicator()),
|
child: SizedBox(height: height, child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import "package:nexus/controllers/members_by_status_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
import "package:nexus/helpers/extensions/string_to_color.dart";
|
import "package:nexus/helpers/extensions/string_to_color.dart";
|
||||||
import "package:nexus/models/configs/members_by_status_config.dart";
|
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
import "package:nexus/models/membership_status.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
@ -21,7 +20,7 @@ class MemberList extends HookConsumerWidget {
|
||||||
final status = useState(MembershipStatus.join);
|
final status = useState(MembershipStatus.join);
|
||||||
final membersProvider = ref.watch(
|
final membersProvider = ref.watch(
|
||||||
MembersByStatusController.provider(
|
MembersByStatusController.provider(
|
||||||
MembersByStatusConfig(roomId: roomId, status: status.value),
|
.new(roomId: roomId, status: status.value),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -33,7 +32,7 @@ class MemberList extends HookConsumerWidget {
|
||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
leading: Icon(Icons.people),
|
leading: Icon(Icons.people),
|
||||||
title: Text("Members"),
|
title: Text("Members"),
|
||||||
actionsPadding: EdgeInsets.only(right: 4),
|
actionsPadding: .only(right: 4),
|
||||||
actions: [
|
actions: [
|
||||||
if (Scaffold.of(context).hasEndDrawer)
|
if (Scaffold.of(context).hasEndDrawer)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -44,24 +43,24 @@ class MemberList extends HookConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: .center,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text("Joined"),
|
label: Text("Joined"),
|
||||||
onSelected: (value) => status.value = MembershipStatus.join,
|
onSelected: (value) => status.value = .join,
|
||||||
selected: status.value == MembershipStatus.join,
|
selected: status.value == .join,
|
||||||
),
|
),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text("Invited"),
|
label: Text("Invited"),
|
||||||
onSelected: (value) => status.value = MembershipStatus.invite,
|
onSelected: (value) => status.value = .invite,
|
||||||
selected: status.value == MembershipStatus.invite,
|
selected: status.value == .invite,
|
||||||
),
|
),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text("Banned"),
|
label: Text("Banned"),
|
||||||
onSelected: (value) => status.value = MembershipStatus.ban,
|
onSelected: (value) => status.value = .ban,
|
||||||
selected: status.value == MembershipStatus.ban,
|
selected: status.value == .ban,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -93,15 +92,15 @@ class MemberList extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
displayName ?? member.stateKey!.localpart,
|
displayName ?? member.stateKey!.localpart,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
style: TextStyle(
|
style: .new(
|
||||||
color: member.stateKey!.colorHash,
|
color: member.stateKey!.colorHash,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: .bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
member.stateKey!,
|
member.stateKey!,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class MessageImage extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) => ExpandableImage(
|
Widget build(BuildContext context, WidgetRef ref) => ExpandableImage(
|
||||||
url.toString(),
|
url.toString(),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: .all(.circular(8)),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedNetworkImage(
|
image: CachedNetworkImage(
|
||||||
url.toString(),
|
url.toString(),
|
||||||
|
|
@ -47,7 +47,7 @@ class MessageImage extends ConsumerWidget {
|
||||||
errorBuilder: (context, error, stackTrace) => Center(
|
errorBuilder: (context, error, stackTrace) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Image Failed to Load",
|
"Image Failed to Load",
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
style: .new(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ class AudioPlayer extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final player = useMemoized(
|
final player = useMemoized(
|
||||||
() => Player(
|
() => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)),
|
||||||
configuration: PlayerConfiguration(bufferSize: 128 * 1024 * 1024),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final playing = useState(false);
|
final playing = useState(false);
|
||||||
|
|
@ -66,7 +64,7 @@ class AudioPlayer extends HookConsumerWidget {
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsGeometry.only(left: 8, right: 16),
|
padding: .only(left: 8, right: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -88,7 +86,7 @@ class AudioPlayer extends HookConsumerWidget {
|
||||||
: duration.value.inMilliseconds.toDouble(),
|
: duration.value.inMilliseconds.toDouble(),
|
||||||
value: position.value.inMilliseconds.toDouble(),
|
value: position.value.inMilliseconds.toDouble(),
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
player.seek(Duration(milliseconds: value.toInt())),
|
player.seek(.new(milliseconds: value.toInt())),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ class VideoPlayer extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final player = useMemoized(
|
final player = useMemoized(
|
||||||
() => Player(
|
() => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)),
|
||||||
configuration: PlayerConfiguration(bufferSize: 128 * 1024 * 1024),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final controller = useMemoized(() => VideoController(player));
|
final controller = useMemoized(() => VideoController(player));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import "package:nexus/controllers/reactions_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/configs/reactions_config.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -24,7 +23,7 @@ class ReactionRow extends ConsumerWidget {
|
||||||
|
|
||||||
return switch (ref.watch(
|
return switch (ref.watch(
|
||||||
ReactionsController.provider(
|
ReactionsController.provider(
|
||||||
ReactionsConfig(roomId: event.roomId, eventRowId: event.rowId),
|
.new(roomId: event.roomId, eventRowId: event.rowId),
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
AsyncData(value: final IMap<String, IList<String>>? reactors) ||
|
AsyncData(value: final IMap<String, IList<String>>? reactors) ||
|
||||||
|
|
@ -47,7 +46,7 @@ class ReactionRow extends ConsumerWidget {
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
label: Row(
|
label: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
|
|
@ -64,15 +63,9 @@ class ReactionRow extends ConsumerWidget {
|
||||||
ref.watch(CrossCacheController.provider),
|
ref.watch(CrossCacheController.provider),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(reaction, overflow: .ellipsis),
|
||||||
reaction,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
count.toString(),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
|
Text(count.toString(), overflow: .ellipsis),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onSelected: enabled.value
|
onSelected: enabled.value
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,11 @@ class EventRenderer extends ConsumerWidget {
|
||||||
textOnly: textOnly,
|
textOnly: textOnly,
|
||||||
),
|
),
|
||||||
|
|
||||||
MembershipContent content =>
|
MembershipContent content => switch (event.previousContent) {
|
||||||
event.previousContent is MembershipContent &&
|
MembershipContent(:final status) =>
|
||||||
(event.previousContent as MembershipContent).status ==
|
status == content.status ? null : MembershipRenderer(event),
|
||||||
content.status
|
_ => MembershipRenderer(event),
|
||||||
? null
|
},
|
||||||
: MembershipRenderer(event),
|
|
||||||
|
|
||||||
AvatarContent() => GenericEventRenderer(Icons.interests, [
|
AvatarContent() => GenericEventRenderer(Icons.interests, [
|
||||||
MessageDisplayname(event),
|
MessageDisplayname(event),
|
||||||
|
|
@ -81,12 +80,12 @@ class EventRenderer extends ConsumerWidget {
|
||||||
Text("created the room"),
|
Text("created the room"),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
PowerLevelsContent() => GenericEventRenderer(Icons.add, [
|
PowerLevelsContent() => GenericEventRenderer(Icons.power, [
|
||||||
MessageDisplayname(event),
|
MessageDisplayname(event),
|
||||||
Text("changed the room's power levels"),
|
Text("changed the room's power levels"),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
JoinRulesContent() => GenericEventRenderer(Icons.add, [
|
JoinRulesContent() => GenericEventRenderer(Icons.rule, [
|
||||||
MessageDisplayname(event),
|
MessageDisplayname(event),
|
||||||
Text("changed the room's join rules"),
|
Text("changed the room's join rules"),
|
||||||
]),
|
]),
|
||||||
|
|
@ -101,10 +100,10 @@ class EventRenderer extends ConsumerWidget {
|
||||||
MessageDisplayname(event),
|
MessageDisplayname(event),
|
||||||
Text(
|
Text(
|
||||||
"changed the room's history visibility to ${switch (historyVisibility) {
|
"changed the room's history visibility to ${switch (historyVisibility) {
|
||||||
HistoryVisibility.invited => "since invited",
|
.invited => "since invited",
|
||||||
HistoryVisibility.joined => "since joined",
|
.joined => "since joined",
|
||||||
HistoryVisibility.shared => "all history visible (shared)",
|
.shared => "all history visible (shared)",
|
||||||
HistoryVisibility.worldReadable => "all history visible (world readable)",
|
.worldReadable => "all history visible (world readable)",
|
||||||
}}",
|
}}",
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
@ -158,7 +157,7 @@ class EventRenderer extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
if (child != null) ...[
|
if (child != null) ...[
|
||||||
if (textOnly)
|
if (textOnly)
|
||||||
|
|
@ -168,7 +167,7 @@ class EventRenderer extends ConsumerWidget {
|
||||||
onSecondaryTapUp: contextMenuCallback,
|
onSecondaryTapUp: contextMenuCallback,
|
||||||
onLongPressStart: contextMenuCallback,
|
onLongPressStart: contextMenuCallback,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: isGrouped ? EdgeInsets.zero : EdgeInsets.only(top: 8),
|
padding: isGrouped ? .zero : .only(top: 8),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -183,12 +182,7 @@ class EventRenderer extends ConsumerWidget {
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
].map(
|
].map((child) => Padding(padding: .only(left: 4), child: child)),
|
||||||
(child) => Padding(
|
|
||||||
padding: EdgeInsetsGeometry.only(left: 4),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
] else if (textOnly)
|
] else if (textOnly)
|
||||||
Text("Unknown event type", style: errorStyle),
|
Text("Unknown event type", style: errorStyle),
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,11 @@ class GenericEventRenderer extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Padding(
|
Widget build(BuildContext context) => Padding(
|
||||||
padding: EdgeInsets.only(bottom: 8),
|
padding: .only(bottom: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(padding: .symmetric(horizontal: 4), child: Icon(icon)),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: Icon(icon),
|
|
||||||
),
|
|
||||||
Expanded(child: Wrap(spacing: 4, children: children)),
|
Expanded(child: Wrap(spacing: 4, children: children)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
import "package:nexus/helpers/extensions/string_to_color.dart";
|
import "package:nexus/helpers/extensions/string_to_color.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
|
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
|
||||||
import "package:nexus/widgets/renderers/generic_event.dart";
|
import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
|
|
||||||
|
|
@ -29,24 +28,21 @@ class MembershipRenderer extends StatelessWidget {
|
||||||
globalPosition: details.globalPosition,
|
globalPosition: details.globalPosition,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
content.displayName ?? event.stateKey!.localpart,
|
content.displayName ?? event.stateKey!.localpart,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: .new(color: event.sender.colorHash, fontWeight: .bold),
|
||||||
color: event.sender.colorHash,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
"${switch (content.status) {
|
"${switch (content.status) {
|
||||||
MembershipStatus.invite => "was invited to",
|
.invite => "was invited to",
|
||||||
MembershipStatus.join => "joined",
|
.join => "joined",
|
||||||
MembershipStatus.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"),
|
.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"),
|
||||||
MembershipStatus.ban => "was banned from",
|
.ban => "was banned from",
|
||||||
MembershipStatus.knock => "asked to join",
|
.knock => "asked to join",
|
||||||
}} the room${event.sender == event.stateKey ? "" : " by "}",
|
}} the room${event.sender == event.stateKey ? "" : " by "}",
|
||||||
),
|
),
|
||||||
if (event.sender != event.stateKey) MessageDisplayname(event),
|
if (event.sender != event.stateKey) MessageDisplayname(event),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import "package:nexus/models/content/encrypted.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/content/sticker.dart";
|
import "package:nexus/models/content/sticker.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_event_request.dart";
|
|
||||||
import "package:nexus/widgets/file_card.dart";
|
import "package:nexus/widgets/file_card.dart";
|
||||||
import "package:nexus/widgets/html/html.dart";
|
import "package:nexus/widgets/html/html.dart";
|
||||||
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
|
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
|
||||||
|
|
@ -49,19 +48,19 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
format(event.timestamp),
|
format(event.timestamp),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey),
|
style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final textStyle = TextStyle(
|
final textStyle = TextStyle(
|
||||||
fontSize: event.localContent?.bigEmoji == true ? 32 : null,
|
fontSize: event.localContent?.bigEmoji == true ? 32 : null,
|
||||||
fontStyle: event.content is EmoteMessageContent ? FontStyle.italic : null,
|
fontStyle: event.content is EmoteMessageContent ? .italic : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (!textOnly)
|
if (!textOnly)
|
||||||
|
|
@ -72,7 +71,7 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
if (!isGrouped && !textOnly)
|
if (!isGrouped && !textOnly)
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -83,7 +82,7 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Card(
|
Card(
|
||||||
margin: textOnly ? EdgeInsets.zero : EdgeInsets.only(bottom: 4),
|
margin: textOnly ? .zero : .only(bottom: 4),
|
||||||
color: textOnly
|
color: textOnly
|
||||||
? Colors.transparent
|
? Colors.transparent
|
||||||
: ref.watch(
|
: ref.watch(
|
||||||
|
|
@ -99,24 +98,21 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
elevation: textOnly ? 0 : null,
|
elevation: textOnly ? 0 : null,
|
||||||
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: textOnly ? EdgeInsets.zero : EdgeInsets.all(12),
|
padding: textOnly ? .zero : .all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
if (!textOnly && event.replyTo != null)
|
if (!textOnly && event.replyTo != null)
|
||||||
Card(
|
Card(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: .only(bottom: 8),
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTapReply,
|
onTap: onTapReply,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsGeometry.symmetric(
|
padding: .symmetric(vertical: 8, horizontal: 12),
|
||||||
vertical: 8,
|
|
||||||
horizontal: 12,
|
|
||||||
),
|
|
||||||
child: switch (ref.watch(
|
child: switch (ref.watch(
|
||||||
EventController.provider(
|
EventController.provider(
|
||||||
GetEventRequest(
|
.new(
|
||||||
roomId: event.roomId,
|
roomId: event.roomId,
|
||||||
eventId: event.replyTo!,
|
eventId: event.replyTo!,
|
||||||
),
|
),
|
||||||
|
|
@ -145,12 +141,10 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
? Text(
|
? Text(
|
||||||
body,
|
body,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
)
|
)
|
||||||
: ConstrainedBox(
|
: ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(
|
constraints: .loose(.square(200)),
|
||||||
Size.square(200),
|
|
||||||
),
|
|
||||||
child: MessageImage(
|
child: MessageImage(
|
||||||
url.mxcToHttps(
|
url.mxcToHttps(
|
||||||
ref.watch(
|
ref.watch(
|
||||||
|
|
@ -199,9 +193,9 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
:final formattedBody,
|
:final formattedBody,
|
||||||
:final format,
|
:final format,
|
||||||
) => Column(
|
) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
format == MessageFormat.html && !textOnly
|
format == .html && !textOnly
|
||||||
? Html(
|
? Html(
|
||||||
roomId: event.roomId,
|
roomId: event.roomId,
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
|
|
@ -243,9 +237,7 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
final url? => ConstrainedBox(
|
final url? => ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(
|
constraints: .loose(.square(500)),
|
||||||
Size.square(500),
|
|
||||||
),
|
|
||||||
child: switch (event.content) {
|
child: switch (event.content) {
|
||||||
VideoMessageContent(:final info) =>
|
VideoMessageContent(:final info) =>
|
||||||
VideoPlayer(url, info),
|
VideoPlayer(url, info),
|
||||||
|
|
@ -286,7 +278,7 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
MessageContent(:final body) => Row(
|
MessageContent(:final body) => Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
Text("Unknown message type:", style: errorStyle),
|
Text("Unknown message type:", style: errorStyle),
|
||||||
Text(body),
|
Text(body),
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,17 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
: () => showDialog(
|
: () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(
|
builder: (context) => Dialog(
|
||||||
constraints: BoxConstraints.loose(Size.fromWidth(400)),
|
constraints: .loose(.fromWidth(400)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsGeometry.all(24),
|
padding: .all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
if (room.metadata?.avatar != null)
|
if (room.metadata?.avatar != null)
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
|
|
@ -71,7 +71,7 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
room.metadata?.name ?? "Unnamed Room",
|
room.metadata?.name ?? "Unnamed Room",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
|
|
@ -105,18 +105,18 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
title: room == null
|
title: room == null
|
||||||
? null
|
? null
|
||||||
: Column(
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
room.metadata?.name ?? "Unnamed Room",
|
room.metadata?.name ?? "Unnamed Room",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
if (room.metadata?.topic?.isNotEmpty == true)
|
if (room.metadata?.topic?.isNotEmpty == true)
|
||||||
Text(
|
Text(
|
||||||
room.metadata!.topic!,
|
room.metadata!.topic!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: .ellipsis,
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/controllers/via_controller.dart";
|
import "package:nexus/controllers/via_controller.dart";
|
||||||
import "package:nexus/models/configs/power_level_config.dart";
|
import "package:nexus/models/configs/power_level_config.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/requests/report_request.dart";
|
|
||||||
import "package:nexus/widgets/composer/composer.dart";
|
import "package:nexus/widgets/composer/composer.dart";
|
||||||
import "package:nexus/widgets/emoji_picker_button.dart";
|
import "package:nexus/widgets/emoji_picker_button.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
|
|
@ -79,10 +77,73 @@ class RoomChat extends HookConsumerWidget {
|
||||||
|
|
||||||
final listController = useRef(ListController());
|
final listController = useRef(ListController());
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
|
final controllerData = ref.watch(controllerProvider);
|
||||||
|
|
||||||
|
final topEventBeforeLoad = useState<String?>(null);
|
||||||
|
|
||||||
|
Future<void> loadOlder() async {
|
||||||
|
if (controllerData case AsyncData(:final value)) {
|
||||||
|
topEventBeforeLoad.value = value.firstOrNull?.eventId;
|
||||||
|
await notifier.loadOlder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
ref
|
||||||
|
.read(controllerProvider.future)
|
||||||
|
.then(
|
||||||
|
(_) => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.jumpTo(
|
||||||
|
scrollController.position.maxScrollExtent - .000001,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [scrollController.hasClients]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (controllerData case AsyncData(
|
||||||
|
:final value,
|
||||||
|
) when scrollController.hasClients) {
|
||||||
|
if (topEventBeforeLoad.value != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
final index = value.indexWhere(
|
||||||
|
(event) => event.eventId == topEventBeforeLoad.value,
|
||||||
|
);
|
||||||
|
if (index != -1) {
|
||||||
|
listController.value.jumpToItem(
|
||||||
|
index: index,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topEventBeforeLoad.value = null;
|
||||||
|
});
|
||||||
|
} else if (scrollController.position.atEdge &&
|
||||||
|
scrollController.position.pixels != 0) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.jumpTo(
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [controllerData]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
Future<void> listener() async {
|
Future<void> listener() async {
|
||||||
if (!scrollController.position.atEdge) return;
|
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
RoomsController.provider.select((value) => value[roomId]),
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
|
|
@ -90,20 +151,21 @@ class RoomChat extends HookConsumerWidget {
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
|
|
||||||
if (scrollController.position.pixels == 0) {
|
if (scrollController.position.pixels == 0) {
|
||||||
await client.markRead(room);
|
if (room.hasMore) {
|
||||||
|
await loadOlder();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (room.hasMore) await notifier.loadOlder();
|
await client.markRead(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollController.addListener(listener);
|
scrollController.addListener(listener);
|
||||||
return () => scrollController.removeListener(listener);
|
return () => scrollController.removeListener(listener);
|
||||||
}, [roomId]);
|
}, [roomId, controllerData]);
|
||||||
|
|
||||||
final composerNode = useFocusNode(
|
final composerNode = useFocusNode(
|
||||||
onKeyEvent: (_, event) {
|
onKeyEvent: (_, event) {
|
||||||
if (event is KeyDownEvent &&
|
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
||||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
|
||||||
relatedEvent.value = null;
|
relatedEvent.value = null;
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +180,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
return [
|
return [
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig(eventType: EventType.reaction, roomId: roomId),
|
.new(eventType: .reaction, roomId: roomId),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
|
@ -134,7 +196,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
value["m.recent_emoji"]
|
value["m.recent_emoji"]
|
||||||
?.content["recent_emoji"] ??
|
?.content["recent_emoji"] ??
|
||||||
[],
|
[],
|
||||||
).map((entry) => entry["emoji"]),
|
).map((entry) => entry["emoji"]).toIList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"👍",
|
"👍",
|
||||||
|
|
@ -167,13 +229,13 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig(eventType: EventType.message, roomId: roomId),
|
PowerLevelConfig(eventType: .message, roomId: roomId),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
relatedEvent.value = event;
|
relatedEvent.value = event;
|
||||||
relationType.value = RelationType.reply;
|
relationType.value = .reply;
|
||||||
composerNode.requestFocus();
|
composerNode.requestFocus();
|
||||||
},
|
},
|
||||||
child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
|
child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
|
||||||
|
|
@ -182,7 +244,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
relatedEvent.value = event;
|
relatedEvent.value = event;
|
||||||
relationType.value = RelationType.edit;
|
relationType.value = .edit;
|
||||||
composerNode.requestFocus();
|
composerNode.requestFocus();
|
||||||
},
|
},
|
||||||
child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
|
child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
|
||||||
|
|
@ -207,10 +269,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig.redaction(
|
.redaction(targetUser: event.sender, roomId: roomId),
|
||||||
targetUser: event.sender,
|
|
||||||
roomId: roomId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
|
@ -222,8 +281,8 @@ class RoomChat extends HookConsumerWidget {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("Delete Message"),
|
title: Text("Delete Message"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this message? This can not be reversed.",
|
"Are you sure you want to delete this message? This can not be reversed.",
|
||||||
|
|
@ -261,7 +320,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Icons.delete, color: danger),
|
leading: Icon(Icons.delete, color: danger),
|
||||||
title: Text("Delete", style: TextStyle(color: danger)),
|
title: Text("Delete", style: .new(color: danger)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
|
@ -273,8 +332,8 @@ class RoomChat extends HookConsumerWidget {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("Report"),
|
title: Text("Report"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Report this event to your server administrators, who can take action like banning this server or room.",
|
"Report this event to your server administrators, who can take action like banning this server or room.",
|
||||||
|
|
@ -297,7 +356,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
client.reportEvent(
|
client.reportEvent(
|
||||||
ReportRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
eventId: event.eventId,
|
eventId: event.eventId,
|
||||||
reason: reasonController.text.isEmpty
|
reason: reasonController.text.isEmpty
|
||||||
|
|
@ -316,14 +375,12 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Icons.report, color: danger),
|
leading: Icon(Icons.report, color: danger),
|
||||||
title: Text("Report", style: TextStyle(color: danger)),
|
title: Text("Report", style: .new(color: danger)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
].toIList();
|
].toIList();
|
||||||
}
|
}
|
||||||
|
|
||||||
final controllerData = ref.watch(controllerProvider);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: RoomAppbar(
|
appBar: RoomAppbar(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
|
|
@ -341,16 +398,23 @@ class RoomChat extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
padding: .symmetric(horizontal: 12),
|
||||||
child: switch (controllerData) {
|
child: switch (controllerData) {
|
||||||
AsyncData(:final value) ||
|
AsyncData(:final value) ||
|
||||||
AsyncLoading(:final value?) => CustomScrollView(
|
AsyncLoading(:final value?) => CustomScrollView(
|
||||||
reverse: true,
|
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPadding(
|
SliverToBoxAdapter(
|
||||||
padding: EdgeInsetsGeometry.only(
|
child: Padding(
|
||||||
bottom: composerSize.value,
|
padding: .symmetric(vertical: 36),
|
||||||
|
child: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controllerData is AsyncData
|
||||||
|
? loadOlder
|
||||||
|
: null,
|
||||||
|
child: Text("Load More"),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -359,7 +423,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
itemCount: value.length,
|
itemCount: value.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final event = value[index];
|
final event = value[index];
|
||||||
final previousEvent = value.getOrNull(index + 1);
|
final previousEvent = value.getOrNull(index - 1);
|
||||||
return FlashWrapper(
|
return FlashWrapper(
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
event,
|
||||||
|
|
@ -371,19 +435,15 @@ class RoomChat extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration: (_) =>
|
duration: (_) => .new(milliseconds: 700),
|
||||||
Duration(milliseconds: 700),
|
|
||||||
curve: (_) => Curves.easeInOut,
|
curve: (_) => Curves.easeInOut,
|
||||||
);
|
);
|
||||||
flashingEvent.value = replyId;
|
flashingEvent.value = replyId;
|
||||||
await Future.delayed(
|
await Future.delayed(.new(seconds: 1), () {
|
||||||
Duration(seconds: 1),
|
|
||||||
() {
|
|
||||||
if (flashingEvent.value == replyId) {
|
if (flashingEvent.value == replyId) {
|
||||||
flashingEvent.value = null;
|
flashingEvent.value = null;
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
isGrouped:
|
isGrouped:
|
||||||
|
|
@ -401,18 +461,8 @@ class RoomChat extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
SliverToBoxAdapter(
|
SliverPadding(
|
||||||
child: Padding(
|
padding: .only(bottom: composerSize.value),
|
||||||
padding: EdgeInsets.symmetric(vertical: 36),
|
|
||||||
child: Center(
|
|
||||||
child: controllerData is AsyncLoading
|
|
||||||
? Loading()
|
|
||||||
: ElevatedButton(
|
|
||||||
onPressed: notifier.loadOlder,
|
|
||||||
child: Text("Load More"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class RoomMenu extends ConsumerWidget {
|
||||||
final vias = ref.watch(ViaController.provider(room!));
|
final vias = ref.watch(ViaController.provider(room!));
|
||||||
|
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(
|
.new(
|
||||||
text:
|
text:
|
||||||
"matrix:roomid/${room!.metadata?.id.substring(1)}$vias)",
|
"matrix:roomid/${room!.metadata?.id.substring(1)}$vias)",
|
||||||
),
|
),
|
||||||
|
|
@ -63,7 +63,7 @@ class RoomMenu extends ConsumerWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
final snackbar = ScaffoldMessenger.of(context)
|
final snackbar = ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
SnackBar(
|
.new(
|
||||||
content: Text("Leaving room..."),
|
content: Text("Leaving room..."),
|
||||||
duration: Duration(days: 1),
|
duration: Duration(days: 1),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,14 @@ class Sidebar extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(space.title),
|
label: Text(space.title),
|
||||||
padding: EdgeInsets.only(top: 4),
|
padding: .only(top: 4),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
trailingAtBottom: true,
|
trailingAtBottom: true,
|
||||||
trailing: Padding(
|
trailing: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: .symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -136,10 +136,7 @@ class Sidebar extends HookConsumerWidget {
|
||||||
|
|
||||||
selectedSpace.title,
|
selectedSpace.title,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(selectedSpace.title, overflow: .ellipsis),
|
||||||
selectedSpace.title,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
actions: [
|
actions: [
|
||||||
RoomMenu(
|
RoomMenu(
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,24 @@ class UrlPreview extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox(
|
Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(Size.fromWidth(400)),
|
constraints: .loose(.fromWidth(400)),
|
||||||
child: ref
|
child: ref
|
||||||
.watch(UrlPreviewController.provider(link))
|
.watch(UrlPreviewController.provider(link))
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (preview) => preview == null
|
data: (preview) => preview == null
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: InkWell(
|
: InkWell(
|
||||||
onTap: () => ref
|
onTap: () =>
|
||||||
.watch(LaunchHelper.provider)
|
ref.watch(LaunchHelper.provider).launchUrl(.parse(link)),
|
||||||
.launchUrl(Uri.parse(link)),
|
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.symmetric(vertical: 4),
|
margin: .symmetric(vertical: 4),
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceContainerHighest,
|
).colorScheme.surfaceContainerHighest,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsGeometry.all(16),
|
padding: .all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
if (preview.title != null)
|
if (preview.title != null)
|
||||||
|
|
@ -45,9 +44,7 @@ class UrlPreview extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
if (preview.imageUrl != null)
|
if (preview.imageUrl != null)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: .all(.circular(8)),
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
child: Image(
|
child: Image(
|
||||||
errorBuilder: (_, _, _) => SizedBox.shrink(),
|
errorBuilder: (_, _, _) => SizedBox.shrink(),
|
||||||
width: preview.width,
|
width: preview.width,
|
||||||
|
|
@ -56,7 +53,7 @@ class UrlPreview extends ConsumerWidget {
|
||||||
ref.watch(CrossCacheController.provider),
|
ref.watch(CrossCacheController.provider),
|
||||||
headers: ref.headers,
|
headers: ref.headers,
|
||||||
),
|
),
|
||||||
fit: BoxFit.fitWidth,
|
fit: .fitWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,8 @@ import "package:nexus/controllers/profile_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/configs/power_level_config.dart";
|
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
import "package:nexus/models/requests/membership_action.dart";
|
import "package:nexus/models/requests/membership_action.dart";
|
||||||
import "package:nexus/models/requests/set_membership_request.dart";
|
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/expandable_image.dart";
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
|
|
@ -39,8 +36,8 @@ class UserPopover extends ConsumerWidget {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("${toBeginningOfSentenceCase(action.name)} $userId"),
|
title: Text("${toBeginningOfSentenceCase(action.name)} $userId"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text("Are you sure you want to ${action.name} $userId?"),
|
Text("Are you sure you want to ${action.name} $userId?"),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
|
@ -62,7 +59,7 @@ class UserPopover extends ConsumerWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
client
|
client
|
||||||
.setMembership(
|
.setMembership(
|
||||||
SetMembershipRequest(
|
.new(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
roomId: roomId!,
|
roomId: roomId!,
|
||||||
action: action,
|
action: action,
|
||||||
|
|
@ -80,20 +77,18 @@ class UserPopover extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
final actionButton = ButtonStyle(
|
final actionButton = ButtonStyle(
|
||||||
padding: WidgetStatePropertyAll(
|
padding: WidgetStatePropertyAll(.symmetric(horizontal: 24, vertical: 18)),
|
||||||
EdgeInsets.symmetric(horizontal: 24, vertical: 18),
|
|
||||||
),
|
|
||||||
shape: WidgetStatePropertyAll(
|
shape: WidgetStatePropertyAll(
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
RoundedRectangleBorder(borderRadius: .circular(8)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: .stretch,
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: .center,
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -126,7 +121,7 @@ class UserPopover extends ConsumerWidget {
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
loading: SizedBox.shrink,
|
loading: SizedBox.shrink,
|
||||||
data: (profile) => Wrap(
|
data: (profile) => Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: .center,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -135,7 +130,7 @@ class UserPopover extends ConsumerWidget {
|
||||||
))
|
))
|
||||||
Chip(
|
Chip(
|
||||||
label: Text(pronoun.summary),
|
label: Text(pronoun.summary),
|
||||||
labelStyle: TextStyle(
|
labelStyle: .new(
|
||||||
color: theme.colorScheme.onPrimary,
|
color: theme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
color: WidgetStatePropertyAll(
|
color: WidgetStatePropertyAll(
|
||||||
|
|
@ -145,7 +140,7 @@ class UserPopover extends ConsumerWidget {
|
||||||
if (profile.timezone != null)
|
if (profile.timezone != null)
|
||||||
Chip(
|
Chip(
|
||||||
label: Text(profile.timezone!),
|
label: Text(profile.timezone!),
|
||||||
labelStyle: TextStyle(
|
labelStyle: .new(
|
||||||
color: theme.colorScheme.onPrimary,
|
color: theme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
color: WidgetStatePropertyAll(
|
color: WidgetStatePropertyAll(
|
||||||
|
|
@ -162,7 +157,7 @@ class UserPopover extends ConsumerWidget {
|
||||||
if (userId != ref.watch(ClientStateController.provider)?.userId &&
|
if (userId != ref.watch(ClientStateController.provider)?.userId &&
|
||||||
roomId != null)
|
roomId != null)
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: .center,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -175,17 +170,17 @@ class UserPopover extends ConsumerWidget {
|
||||||
|
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig.membershipAction(
|
.membershipAction(
|
||||||
action: MembershipAction.kick,
|
action: .kick,
|
||||||
roomId: roomId!,
|
roomId: roomId!,
|
||||||
targetUser: userId,
|
targetUser: userId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) &&
|
) &&
|
||||||
member.status == MembershipStatus.join ||
|
member.status == .join ||
|
||||||
member.status == MembershipStatus.invite)
|
member.status == .invite)
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () => showMembershipDialog(MembershipAction.kick),
|
onPressed: () => showMembershipDialog(.kick),
|
||||||
label: Text("Kick"),
|
label: Text("Kick"),
|
||||||
style: actionButton.copyWith(
|
style: actionButton.copyWith(
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
|
@ -199,23 +194,19 @@ class UserPopover extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
PowerLevelController.provider(
|
PowerLevelController.provider(
|
||||||
PowerLevelConfig.membershipAction(
|
.membershipAction(
|
||||||
roomId: roomId!,
|
roomId: roomId!,
|
||||||
action: MembershipAction.ban,
|
action: .ban,
|
||||||
targetUser: userId,
|
targetUser: userId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => showMembershipDialog(
|
onPressed: () => showMembershipDialog(
|
||||||
member.status == MembershipStatus.ban
|
member.status == .ban ? .unban : .ban,
|
||||||
? MembershipAction.unban
|
|
||||||
: MembershipAction.ban,
|
|
||||||
),
|
),
|
||||||
icon: Icon(Icons.gavel),
|
icon: Icon(Icons.gavel),
|
||||||
label: Text(
|
label: Text(member.status == .ban ? "Unban" : "Ban"),
|
||||||
member.status == MembershipStatus.ban ? "Unban" : "Ban",
|
|
||||||
),
|
|
||||||
style: actionButton.copyWith(
|
style: actionButton.copyWith(
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
theme.colorScheme.errorContainer,
|
theme.colorScheme.errorContainer,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue