forked from Nexus/nexus
Compare commits
6 changed files with 184 additions and 238 deletions
18
.github/workflows/ios.yml
vendored
18
.github/workflows/ios.yml
vendored
|
|
@ -16,22 +16,24 @@ jobs:
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Flutter
|
- name: Lix GHA Installer Action
|
||||||
uses: subosito/flutter-action@v2
|
uses: samueldr/lix-gha-installer-action@v2026-02-22
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.44.4
|
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: gomuks/go.mod
|
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: |
|
run: |
|
||||||
|
nix develop --command bash -c '
|
||||||
|
set -e
|
||||||
flutter pub get
|
flutter pub get
|
||||||
dart scripts/generate.dart
|
dart scripts/generate.dart
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
|
|
||||||
|
FLUTTER_ROOT="$(dirname "$(dirname "$(readlink -f "$(command -v flutter)")")")"
|
||||||
|
sudo find -L "$FLUTTER_ROOT" -type d -iname "*.framework" -exec chmod -R u+w {} +
|
||||||
|
|
||||||
flutter build ios --release --no-codesign
|
flutter build ios --release --no-codesign
|
||||||
|
'
|
||||||
|
|
||||||
# Unsigned IPA: SideStore/AltStore re-sign this on-device with the
|
# Unsigned IPA: SideStore/AltStore re-sign this on-device with the
|
||||||
# user's own Apple ID at install time, so no certificate is needed here.
|
# user's own Apple ID at install time, so no certificate is needed here.
|
||||||
|
|
|
||||||
20
.github/workflows/macos.yml
vendored
20
.github/workflows/macos.yml
vendored
|
|
@ -16,25 +16,27 @@ jobs:
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Flutter
|
- name: Lix GHA Installer Action
|
||||||
uses: subosito/flutter-action@v2
|
uses: samueldr/lix-gha-installer-action@v2026-02-22
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.44.4
|
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: gomuks/go.mod
|
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: |
|
run: |
|
||||||
|
nix develop --command bash -c '
|
||||||
|
set -e
|
||||||
flutter pub get
|
flutter pub get
|
||||||
dart scripts/generate.dart
|
dart scripts/generate.dart
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
|
|
||||||
|
FLUTTER_ROOT="$(dirname "$(dirname "$(readlink -f "$(command -v flutter)")")")"
|
||||||
|
sudo find -L "$FLUTTER_ROOT" -type d -iname "*.framework" -exec chmod -R u+w {} +
|
||||||
|
|
||||||
flutter build macos --release
|
flutter build macos --release
|
||||||
|
'
|
||||||
|
|
||||||
- name: Upload installer artifact
|
- name: Upload installer artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Nexus.App
|
name: App
|
||||||
path: build/macos/Build/Products/Release/Nexus.app
|
path: build/macos/Build/Products/Release/Nexus.app
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -15,12 +15,12 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
- [x] Platform Support
|
- [ ] Platform Support
|
||||||
- [x] Linux
|
- [x] Linux
|
||||||
- [x] Windows - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus).
|
- [x] Windows
|
||||||
- [x] Android
|
- [x] Android
|
||||||
- [x] MacOS - Unsigned .App only
|
- [ ] MacOS ([#50](https://git.federated.nexus/Nexus/nexus/issues/50))
|
||||||
- [x] iOS - Unsigned .ipa only
|
- [ ] iOS
|
||||||
- [ ] Web (may not be possible)
|
- [ ] Web (may not be possible)
|
||||||
- [x] Login (via OAuth)
|
- [x] Login (via OAuth)
|
||||||
- [x] Rooms / Spaces
|
- [x] Rooms / Spaces
|
||||||
|
|
@ -116,9 +116,6 @@ If you want to try out Nexus, grab one of the following artifacts from CI:
|
||||||
|
|
||||||
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
||||||
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
||||||
- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip)
|
|
||||||
- [Unsigned MacOS App](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/Nexus.App.zip)
|
|
||||||
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
|
||||||
- Flatpak
|
- Flatpak
|
||||||
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
||||||
- [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip)
|
- [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip)
|
||||||
|
|
@ -129,8 +126,8 @@ If you want to try out Nexus, grab one of the following artifacts from CI:
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
- On NixOS: Either use direnv and `direnv allow`, or `nix flake develop`
|
- With Nix: Either use direnv and `direnv allow`, or `nix flake develop`
|
||||||
- On other distros: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues.
|
- Without Nix: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues.
|
||||||
|
|
||||||
#### Windows
|
#### Windows
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
iosSdk = iosConfig.targetSdk;
|
iosSdk = iosConfig.targetSdk;
|
||||||
final minVersion = iosConfig.targetVersion;
|
final minVersion = iosConfig.targetVersion;
|
||||||
iosSdkPath = await getXCodeTool(sdkType: iosSdk.type);
|
iosSdkPath = await getXCodeTool(sdkType: iosSdk.type);
|
||||||
|
|
||||||
final archTriple = switch (targetArch) {
|
final archTriple = switch (targetArch) {
|
||||||
Architecture.arm64 => "arm64",
|
Architecture.arm64 => "arm64",
|
||||||
Architecture.x64 => "x86_64",
|
Architecture.x64 => "x86_64",
|
||||||
|
|
@ -37,7 +36,6 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
"Unsupported iOS architecture: $targetArch",
|
"Unsupported iOS architecture: $targetArch",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
iosTargetTriple = iosSdk == IOSSdk.iPhoneSimulator
|
iosTargetTriple = iosSdk == IOSSdk.iPhoneSimulator
|
||||||
? "$archTriple-apple-ios$minVersion.0-simulator"
|
? "$archTriple-apple-ios$minVersion.0-simulator"
|
||||||
: "$archTriple-apple-ios$minVersion.0";
|
: "$archTriple-apple-ios$minVersion.0";
|
||||||
|
|
@ -50,7 +48,8 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
break;
|
break;
|
||||||
case OS.macOS:
|
case OS.macOS:
|
||||||
libFileName = "libgomuks.dylib";
|
libFileName = "libgomuks.dylib";
|
||||||
extraEnv = {"SDKROOT": await getXCodeTool()};
|
|
||||||
|
extraEnv = {"CC": await getXCodeTool(findTool: "clang")};
|
||||||
break;
|
break;
|
||||||
case OS.windows:
|
case OS.windows:
|
||||||
libFileName = "libgomuks.dll";
|
libFileName = "libgomuks.dll";
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,27 @@ import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
Future<bool>? _loadingOlder;
|
|
||||||
RoomChatController(this.roomId);
|
RoomChatController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<Event>?> build() async {
|
Future<IList<Event>?> build() async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final initialRoom = ref.watch(
|
final room = ref.watch(
|
||||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (initialRoom == null) return null;
|
if (room == null) return null;
|
||||||
Room room = initialRoom;
|
|
||||||
|
|
||||||
if (!room.hasFetchedState) {
|
if (!room.hasFetchedState) {
|
||||||
final state = await client.getRoomState(.new(roomId: roomId));
|
final state = await client.getRoomState(.new(roomId: roomId));
|
||||||
|
|
||||||
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
||||||
room = ref.read(RoomsController.provider)[roomId] ?? room;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// While there are under 20 events, try to load more
|
// While there are under 20 events, try to load more
|
||||||
// until there's no more or the conditions are met.
|
// until there's no more or the conditions are met.
|
||||||
if (room.hasMore && room.timeline.length < 20) {
|
if (room.hasMore && room.timeline.length < 20) {
|
||||||
await loadOlder();
|
loadOlder();
|
||||||
room = ref.read(RoomsController.provider)[roomId] ?? room;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return room.timeline
|
return room.timeline
|
||||||
|
|
@ -78,22 +74,18 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> loadOlder() => _loadingOlder ??= _loadOlder().whenComplete(() {
|
Future<bool> loadOlder() async {
|
||||||
_loadingOlder = null;
|
final timelineKeys = ref
|
||||||
});
|
.read(RoomsController.provider.select((value) => value[roomId]))
|
||||||
|
?.timeline
|
||||||
Future<bool> _loadOlder() async {
|
.keys;
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
|
||||||
if (room == null || !room.hasMore) return false;
|
|
||||||
|
|
||||||
final timelineKeys = room.timeline.keys;
|
|
||||||
final response = await ref
|
final response = await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.paginate(
|
.paginate(
|
||||||
.new(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
maxTimelineId: timelineKeys.isNotEmpty
|
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||||
? timelineKeys.reduce(min)
|
? timelineKeys?.reduce(min)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -72,47 +72,23 @@ class RoomChat extends HookConsumerWidget {
|
||||||
|
|
||||||
final controllerProvider = RoomChatController.provider(roomId);
|
final controllerProvider = RoomChatController.provider(roomId);
|
||||||
final notifier = ref.watch(controllerProvider.notifier);
|
final notifier = ref.watch(controllerProvider.notifier);
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
|
final listController = useRef(ListController());
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final controllerData = ref.watch(controllerProvider);
|
final controllerData = ref.watch(controllerProvider);
|
||||||
final historyListController = useRef(ListController());
|
|
||||||
final liveListController = useRef(ListController());
|
final topEventBeforeLoad = useState<String?>(null);
|
||||||
final timelineCenterKey = useMemoized(GlobalKey.new);
|
|
||||||
final timelineBoundary = useRef<String?>(null);
|
|
||||||
final hasMore = ref.watch(
|
|
||||||
RoomsController.provider.select(
|
|
||||||
(rooms) => rooms[roomId]?.hasMore ?? false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> jumpToId(String eventId) async {
|
Future<void> jumpToId(String eventId) async {
|
||||||
var events = ref.read(controllerProvider).value;
|
final index = controllerData.value?.indexWhere(
|
||||||
while (events?.any((event) => event.eventId == eventId) != true) {
|
(element) => element.eventId == eventId,
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
|
||||||
if (room == null || !room.hasMore) return;
|
|
||||||
await notifier.loadOlder();
|
|
||||||
await WidgetsBinding.instance.endOfFrame;
|
|
||||||
events = ref.read(controllerProvider).value;
|
|
||||||
}
|
|
||||||
if (events == null || !scrollController.hasClients) return;
|
|
||||||
|
|
||||||
final sourceIndex = events.indexWhere(
|
|
||||||
(event) => event.eventId == eventId,
|
|
||||||
);
|
);
|
||||||
final boundaryIndex = events.indexWhere(
|
if (index == null) return;
|
||||||
(event) => event.eventId == timelineBoundary.value,
|
|
||||||
);
|
|
||||||
if (sourceIndex < 0 || boundaryIndex < 0) return;
|
|
||||||
|
|
||||||
final listController = sourceIndex <= boundaryIndex
|
listController.value.animateToItem(
|
||||||
? historyListController.value
|
index: index,
|
||||||
: liveListController.value;
|
|
||||||
if (!listController.isAttached) return;
|
|
||||||
listController.animateToItem(
|
|
||||||
index: sourceIndex <= boundaryIndex
|
|
||||||
? boundaryIndex - sourceIndex
|
|
||||||
: sourceIndex - boundaryIndex - 1,
|
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration: (_) => .new(milliseconds: 700),
|
duration: (_) => .new(milliseconds: 700),
|
||||||
|
|
@ -127,11 +103,87 @@ class RoomChat extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadOlder() async {
|
Future<void> loadOlder() async {
|
||||||
if (controllerData case AsyncData(value: final _?)) {
|
if (controllerData case AsyncData(:final value?)) {
|
||||||
|
topEventBeforeLoad.value = value.firstOrNull?.eventId;
|
||||||
await notifier.loadOlder();
|
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(() {
|
||||||
|
Future<void> listener() async {
|
||||||
|
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
|
);
|
||||||
|
if (room == null) return;
|
||||||
|
|
||||||
|
if (scrollController.position.pixels == 0) {
|
||||||
|
if (room.hasMore) {
|
||||||
|
await loadOlder();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await client.markRead(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
}, [roomId, controllerData]);
|
||||||
|
|
||||||
final composerNode = useFocusNode(
|
final composerNode = useFocusNode(
|
||||||
onKeyEvent: (_, event) {
|
onKeyEvent: (_, event) {
|
||||||
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
||||||
|
|
@ -413,53 +465,38 @@ class RoomChat extends HookConsumerWidget {
|
||||||
padding: .symmetric(horizontal: 4),
|
padding: .symmetric(horizontal: 4),
|
||||||
child: switch (controllerData) {
|
child: switch (controllerData) {
|
||||||
AsyncData(:final value?) ||
|
AsyncData(:final value?) ||
|
||||||
AsyncLoading(:final value?) => Builder(
|
AsyncLoading(:final value?) => CustomScrollView(
|
||||||
builder: (_) {
|
keyboardDismissBehavior:
|
||||||
if (value.isEmpty) return nothing;
|
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
var boundaryIndex = value.indexWhere(
|
controller: scrollController,
|
||||||
(event) =>
|
slivers: [
|
||||||
event.eventId == timelineBoundary.value,
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: .symmetric(vertical: 36),
|
||||||
|
child: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controllerData is AsyncData
|
||||||
|
? loadOlder
|
||||||
|
: null,
|
||||||
|
child: Text("Load More"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SuperSliverList.builder(
|
||||||
|
listController: listController.value,
|
||||||
|
itemCount: value.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final event = value[index];
|
||||||
|
final previousEvent = value.getOrNull(
|
||||||
|
index - 1,
|
||||||
);
|
);
|
||||||
if (boundaryIndex < 0) {
|
return HighlightWrapper(
|
||||||
boundaryIndex = value.length - 1;
|
|
||||||
timelineBoundary.value =
|
|
||||||
value[boundaryIndex].eventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
final historyLength = boundaryIndex + 1;
|
|
||||||
final liveLength = value.length - historyLength;
|
|
||||||
|
|
||||||
int? historyChildIndex(Key key) {
|
|
||||||
if (key == const ValueKey("history-loader")) {
|
|
||||||
return historyLength;
|
|
||||||
}
|
|
||||||
if (key is! ValueKey<String>) return null;
|
|
||||||
final eventIndex = value.indexWhere(
|
|
||||||
(event) => event.eventId == key.value,
|
|
||||||
);
|
|
||||||
return eventIndex < 0 ||
|
|
||||||
eventIndex >= historyLength
|
|
||||||
? null
|
|
||||||
: historyLength - eventIndex - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int? liveChildIndex(Key key) {
|
|
||||||
if (key is! ValueKey<String>) return null;
|
|
||||||
final eventIndex = value.indexWhere(
|
|
||||||
(event) => event.eventId == key.value,
|
|
||||||
);
|
|
||||||
return eventIndex < historyLength
|
|
||||||
? null
|
|
||||||
: eventIndex - historyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget eventRow(
|
|
||||||
Event event,
|
|
||||||
Event? previousEvent,
|
|
||||||
) => HighlightWrapper(
|
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
event,
|
||||||
onTapReply: () => jumpToId(event.replyTo!),
|
onTapReply: () =>
|
||||||
|
jumpToId(event.replyTo!),
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
isGrouped:
|
isGrouped:
|
||||||
previousEvent?.content
|
previousEvent?.content
|
||||||
|
|
@ -470,60 +507,16 @@ class RoomChat extends HookConsumerWidget {
|
||||||
"${event.sender}${event.pmp?.id}" ==
|
"${event.sender}${event.pmp?.id}" ==
|
||||||
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
||||||
),
|
),
|
||||||
key: ValueKey(event.eventId),
|
|
||||||
isHighlighted:
|
isHighlighted:
|
||||||
highlightedEvent.value == event.eventId,
|
highlightedEvent.value == event.eventId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
reverse: true,
|
|
||||||
center: timelineCenterKey,
|
|
||||||
keyboardDismissBehavior:
|
|
||||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: SizedBox(height: composerSize.value),
|
|
||||||
),
|
|
||||||
SuperSliverList.builder(
|
|
||||||
listController: liveListController.value,
|
|
||||||
itemCount: liveLength,
|
|
||||||
findChildIndexCallback: liveChildIndex,
|
|
||||||
itemBuilder: (_, index) => eventRow(
|
|
||||||
value[historyLength + index],
|
|
||||||
value.getOrNull(
|
|
||||||
historyLength + index - 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SuperSliverList.builder(
|
|
||||||
key: timelineCenterKey,
|
|
||||||
listController: historyListController.value,
|
|
||||||
itemCount: historyLength + 1,
|
|
||||||
findChildIndexCallback: historyChildIndex,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
if (index == historyLength) {
|
|
||||||
return _HistoryLoader(
|
|
||||||
key: const ValueKey("history-loader"),
|
|
||||||
hasMore: hasMore,
|
|
||||||
isLoading:
|
|
||||||
controllerData is AsyncLoading,
|
|
||||||
onLoad: loadOlder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final event =
|
|
||||||
value[historyLength - index - 1];
|
|
||||||
return eventRow(
|
|
||||||
event,
|
|
||||||
value.getOrNull(
|
|
||||||
historyLength - index - 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: .only(bottom: composerSize.value),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
AsyncData() => nothing,
|
AsyncData() => nothing,
|
||||||
AsyncLoading() => Loading(),
|
AsyncLoading() => Loading(),
|
||||||
|
|
@ -571,42 +564,3 @@ class RoomChat extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryLoader extends HookWidget {
|
|
||||||
final bool hasMore;
|
|
||||||
final bool isLoading;
|
|
||||||
final Future<void> Function() onLoad;
|
|
||||||
|
|
||||||
const _HistoryLoader({
|
|
||||||
required this.hasMore,
|
|
||||||
required this.isLoading,
|
|
||||||
required this.onLoad,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
useEffect(() {
|
|
||||||
if (hasMore) {
|
|
||||||
Future.microtask(onLoad);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, const []);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: .symmetric(vertical: 36),
|
|
||||||
child: Center(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: hasMore && !isLoading ? onLoad : null,
|
|
||||||
child: isLoading
|
|
||||||
? SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: Text(hasMore ? "Load More" : "Start of timeline"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue