From 553e45a24ac62d0c9452352b7eb8163e2d50f259 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sun, 18 Feb 2024 15:09:19 -0500 Subject: [PATCH 01/14] updates for docker build and flutter 3.19 --- .dockerignore | 6 +++- .gitignore | 5 ++++ docker/Dockerfile | 30 +++++++------------ scripts/build-android.dart | 2 +- scripts/docker-build-android.sh | 53 +++++++++++++++++++++++++++++---- 5 files changed, 69 insertions(+), 27 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3472aa7a4..3881b5c45 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,7 @@ scripts/docker* docker -build \ No newline at end of file +build +.dart_tool +key.properties +keystore.jks +android diff --git a/.gitignore b/.gitignore index 96ffda369..2ac9ce959 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Docker ignores +key.properties +keystore.jks +.env diff --git a/docker/Dockerfile b/docker/Dockerfile index 9a9a38d7d..1cf9aa53a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,3 @@ -# Builder is not optimized for layer size on purpose -# Allows for modification between layers - FROM ubuntu:22.04 ENV DEBIAN_FRONTEND noninteractive @@ -28,7 +25,7 @@ RUN \ openssl \ wget &&\ # Flutter - wget --quiet https://storage.googleapis.com/flutter_infra_release/releases/beta/linux/flutter_linux_3.12.0-beta.tar.xz -O /tmp/flutter.tar.xz &&\ + wget --quiet https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.0-stable.tar.xz -O /tmp/flutter.tar.xz &&\ mkdir -p /opt &&\ cd /opt &&\ tar xf /tmp/flutter.tar.xz &&\ @@ -51,7 +48,13 @@ RUN \ apt-get install -y --no-install-recommends openjdk-18-jdk openjdk-18-jre &&\ yes | sdkmanager --licenses &&\ sdkmanager --update &&\ + sdkmanager --install "platforms;android-28" &&\ + sdkmanager --install "platforms;android-29" &&\ + sdkmanager --install "platforms;android-30" &&\ + sdkmanager --install "platforms;android-31" &&\ + sdkmanager --install "platforms;android-32" &&\ sdkmanager --install "platforms;android-33" &&\ + sdkmanager --install "platforms;android-34" &&\ sdkmanager --install "build-tools;30.0.3" &&\ sdkmanager --install "ndk;23.1.7779620" &&\ sdkmanager --install "cmake;3.22.1" &&\ @@ -61,19 +64,6 @@ RUN \ # Cleanup apt-get autoremove && apt-get autoclean -WORKDIR /build - -# Copy in prereq files -COPY pubspec.* /build/ - -# Get deps -RUN flutter pub get - -# Add all files -COPY . /build - -# Set up env -RUN echo "# comment" > /build/.env - -# Build -RUN dart scripts/build-android.dart \ No newline at end of file +# Set ownership for flutter +ARG DEV_UID=0 +RUN chown -R ${DEV_UID} /opt/flutter diff --git a/scripts/build-android.dart b/scripts/build-android.dart index 999cd84b3..a419c67f7 100644 --- a/scripts/build-android.dart +++ b/scripts/build-android.dart @@ -26,7 +26,7 @@ void buildRelease() { // Build for Android print('\nStarting Android build...'); - ProcessResult androidResult = Process.runSync('flutter', ['build', 'apk', '--release']); + ProcessResult androidResult = Process.runSync('flutter', ['build', 'apk', '--release', '--no-tree-shake-icons']); stdout.write(androidResult.stdout); stderr.write(androidResult.stderr); diff --git a/scripts/docker-build-android.sh b/scripts/docker-build-android.sh index 46c6b6c52..a667390d2 100755 --- a/scripts/docker-build-android.sh +++ b/scripts/docker-build-android.sh @@ -3,12 +3,55 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) D=$(date +'%Y%m%d.%H%M%S%3N') +set -e + cd "${SCRIPT_DIR}/.." -# Build the apk inside the image +# Create the builder image docker build \ -t thunder-builder \ -f ./docker/Dockerfile \ - . &&\ -# Copy the APK out of the image -mkdir -p ./build/app/outputs/apk/release &&\ -docker cp $(docker create --name tb thunder-builder):/build/build/app/outputs/apk/release/app-release.apk ./build/app/outputs/apk/release/app-release.${D}.apk && docker rm tb >/dev/null + --build-arg="DEV_UID=$(id -u)" \ + . + +# Check docker build folder +mkdir -p ./build/docker + +# Create keystore +if [ ! -f ./android/app/keystore.jks ]; then + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder keytool -genkey -v -keystore ./android/app/keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias thunderdev -keypass password -storepass password -srcstorepass password -noprompt -dname "cn=First Last, ou=Java, o=Oracle, c=US" +fi + +# Make key properties +if [ ! -f ./android/key.properties ]; then + echo ' +storePassword=password +keyPassword=password +keyAlias=thunderdev +storeFile=keystore.jks +' > ./android/key.properties +fi + + +# Build the APK +# docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder git config --global --add safe.directory /opt/flutter +if [ ! -d ./build/docker/.pub-cache ]; then + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter pub get' + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter --disable-analytics' + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'dart --disable-analytics' +fi + +# Create env +if [ ! -f ./.env ]; then + echo "# comment" > ./.env +fi + +# Make path in container +if [ ! -f ./build/docker/.bashrc ]; then + echo ' +export PATH="${PATH}:${HOME}/.pub-cache/bin" +export GRADLE_USER_HOME=${HOME} +' > ./build/docker/.bashrc +fi + +# Build the APK +docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'dart scripts/build-android.dart' From 04122df0964119ea22ab064f410a4a56cb64bc6d Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sun, 18 Feb 2024 15:44:18 -0500 Subject: [PATCH 02/14] reduce flutter from 3.19 to 3.16.9 --- docker/Dockerfile | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1cf9aa53a..83fb00698 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,15 +23,9 @@ RUN \ libglu1-mesa-dev \ git-lfs \ openssl \ - wget &&\ - # Flutter - wget --quiet https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.0-stable.tar.xz -O /tmp/flutter.tar.xz &&\ - mkdir -p /opt &&\ - cd /opt &&\ - tar xf /tmp/flutter.tar.xz &&\ - rm /tmp/flutter.tar.xz &&\ - git config --global --add safe.directory /opt/flutter &&\ - dart pub global activate cider &&\ + wget + +RUN \ # Android SDK apt-get install -y --no-install-recommends \ git \ @@ -60,10 +54,24 @@ RUN \ sdkmanager --install "cmake;3.22.1" &&\ sdkmanager --install platform-tools &&\ sdkmanager --install emulator &&\ - sdkmanager --install tools &&\ - # Cleanup - apt-get autoremove && apt-get autoclean + sdkmanager --install tools + -# Set ownership for flutter ARG DEV_UID=0 -RUN chown -R ${DEV_UID} /opt/flutter +RUN \ + # Flutter + wget --quiet https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.16.9-stable.tar.xz -O /tmp/flutter.tar.xz &&\ + mkdir -p /opt &&\ + cd /opt &&\ + tar xf /tmp/flutter.tar.xz &&\ + rm /tmp/flutter.tar.xz &&\ + git config --global --add safe.directory /opt/flutter &&\ + dart pub global activate cider &&\ + chown -R ${DEV_UID} /opt/flutter + +# Optional add eo language pack +# RUN apt install -y locales language-selector-common +# RUN apt install -y $(check-language-support -l eo) + +# Optional cleanup if above is combined for push +# RUN apt-get autoremove && apt-get autoclean From 265b827539786074bc51de8d09287ec1f5da88b0 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sun, 18 Feb 2024 15:46:56 -0500 Subject: [PATCH 03/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a8a39b1..6c62b6cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ## Changed - Small UI adjustments for account switcher - Dynamic Maximum Zoom Level Based on Image Resolution - contribution from @Niranjan-Dorage +- Update docker build scripts for API 34 and Flutter 3.16.9 - contribution from @Fmstrat ### Fixed - Fixed issue where Thunder was being locked to 60Hz on 120Hz displays on Android From 5b6506dfe332837223e88163e167769261b6ef58 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sun, 18 Feb 2024 15:47:42 -0500 Subject: [PATCH 04/14] docker script cleanup --- scripts/docker-build-android.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/docker-build-android.sh b/scripts/docker-build-android.sh index a667390d2..06c7fb81e 100755 --- a/scripts/docker-build-android.sh +++ b/scripts/docker-build-android.sh @@ -33,7 +33,6 @@ fi # Build the APK -# docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder git config --global --add safe.directory /opt/flutter if [ ! -d ./build/docker/.pub-cache ]; then docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter pub get' docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter --disable-analytics' From 61b5329e3d5c9eec695f44088f372edcc9129789 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sun, 18 Feb 2024 21:34:20 -0500 Subject: [PATCH 05/14] mark read on scroll - no toggle --- lib/community/widgets/post_card.dart | 4 ++++ lib/community/widgets/post_card_list.dart | 1 + lib/feed/view/feed_widget.dart | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index 9683e2fda..5efa508b2 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -24,6 +24,7 @@ class PostCard extends StatefulWidget { final Function(int) onVoteAction; final Function(bool) onSaveAction; final Function(bool) onReadAction; + final Function() onUpAction; final ListingType? listingType; @@ -34,6 +35,7 @@ class PostCard extends StatefulWidget { required this.onVoteAction, required this.onSaveAction, required this.onReadAction, + required this.onUpAction, required this.listingType, required this.indicateRead, }); @@ -98,6 +100,8 @@ class _PostCardState extends State { postViewMedia: widget.postViewMedia, ); } + + widget.onUpAction(); }, onPointerCancel: (event) => {}, onPointerMove: (PointerMoveEvent event) { diff --git a/lib/community/widgets/post_card_list.dart b/lib/community/widgets/post_card_list.dart index eefa61444..9fd4ab163 100644 --- a/lib/community/widgets/post_card_list.dart +++ b/lib/community/widgets/post_card_list.dart @@ -165,6 +165,7 @@ class _PostCardListState extends State { onVoteAction: (int voteType) => widget.onVoteAction(postViewMedia.postView.post.id, voteType), onSaveAction: (bool saved) => widget.onSaveAction(postViewMedia.postView.post.id, saved), onReadAction: (bool read) => widget.onToggleReadAction(postViewMedia.postView.post.id, read), + onUpAction: () {}, listingType: widget.listingType, indicateRead: widget.indicateRead, ); diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index 7a577e4f1..0c58be0b1 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:thunder/community/widgets/post_card.dart'; +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/feed/view/feed_page.dart'; @@ -24,6 +25,7 @@ class FeedPostList extends StatelessWidget { Widget build(BuildContext context) { final ThunderState thunderState = context.read().state; final FeedState state = context.read().state; + final bool isUserLoggedIn = context.read().state.isLoggedIn; // Widget representing the list of posts on the feed return SliverMasonryGrid.count( @@ -67,6 +69,21 @@ class FeedPostList extends StatelessWidget { onReadAction: (bool read) { context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); }, + onUpAction: () { + int past = tabletMode ? 10 : 5; + if (isUserLoggedIn && index > past + 1) { + for (var i = 0; i < index - past; i++) { + if (postViewMedias[i].postView.read != true) { + context.read().add(FeedItemActionedEvent(postId: postViewMedias[i].postView.post.id, postAction: PostAction.read, value: true)); + // Debug + bool read = postViewMedias[i].postView.read; + int postId = postViewMedias[i].postView.post.id; + print("marked read $i $read $postId"); + // /Debug + } + } + } + }, listingType: state.postListingType, indicateRead: thunderState.dimReadPosts, ) From 66f5ba02ab1904ea007c671485cb23c0ec2803d0 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 19 Feb 2024 10:34:52 -0500 Subject: [PATCH 06/14] set up for .19 multi-read --- lib/feed/bloc/feed_bloc.dart | 47 +++++++++++++++++++++++++++++++++ lib/feed/bloc/feed_event.dart | 4 ++- lib/feed/view/feed_widget.dart | 24 +++++++++++------ lib/post/enums/post_action.dart | 1 + lib/post/utils/post.dart | 23 ++++++++++++++++ 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/lib/feed/bloc/feed_bloc.dart b/lib/feed/bloc/feed_bloc.dart index 5dc43fecb..1048aae36 100644 --- a/lib/feed/bloc/feed_bloc.dart +++ b/lib/feed/bloc/feed_bloc.dart @@ -208,6 +208,53 @@ class FeedBloc extends Bloc { state.postViewMedias[existingPostViewMediaIndex].postView = originalPostView; return emit(state.copyWith(status: FeedStatus.failure)); } + case PostAction.multiRead: + List eventPostIds = event.postIds ?? []; + if (eventPostIds.length > 0) { + // Optimistically read the posts + List existingPostViewMediaIndexes = []; + List postIds = []; + List postViewMedias = []; + List originalPostViews = []; + for (int i = 0; i < state.postViewMedias.length; i++) { + if (eventPostIds.contains(state.postViewMedias[i].postView.post.id)) { + existingPostViewMediaIndexes.add(i); + postIds.add(state.postViewMedias[i].postView.post.id); + postViewMedias.add(state.postViewMedias[i]); + originalPostViews.add(state.postViewMedias[i].postView); + } + } + + // Give a slight delay to have the UI perform any navigation first + await Future.delayed(const Duration(milliseconds: 250)); + + try { + for (int i = 0; i < existingPostViewMediaIndexes.length; i++) { + PostView updatedPostView = optimisticallyReadPost(postViewMedias[i], event.value); + state.postViewMedias[existingPostViewMediaIndexes[i]].postView = updatedPostView; + } + + // Emit the state to update UI immediately + emit(state.copyWith(status: FeedStatus.success)); + emit(state.copyWith(status: FeedStatus.fetching)); + + List failed = await markPostsAsRead(postIds, event.value); + if (failed.length == 0) return emit(state.copyWith(status: FeedStatus.success)); + + // Restore the original post contents if not successful + for (int i = 0; i < failed.length; i++) { + state.postViewMedias[existingPostViewMediaIndexes[failed[i]]].postView = originalPostViews[failed[i]]; + } + return emit(state.copyWith(status: FeedStatus.failure)); + } catch (e) { + // Restore the original post contents + // They will all be restored, but this is an unlikely scenario + for (int i = 0; i < existingPostViewMediaIndexes.length; i++) { + state.postViewMedias[existingPostViewMediaIndexes[i]].postView = originalPostViews[i]; + } + return emit(state.copyWith(status: FeedStatus.failure)); + } + } case PostAction.delete: // Optimistically delete the post int existingPostViewMediaIndex = state.postViewMedias.indexWhere((PostViewMedia postViewMedia) => postViewMedia.postView.post.id == event.postId); diff --git a/lib/feed/bloc/feed_event.dart b/lib/feed/bloc/feed_event.dart index 91ca4e7c9..9d32de53f 100644 --- a/lib/feed/bloc/feed_event.dart +++ b/lib/feed/bloc/feed_event.dart @@ -73,6 +73,8 @@ final class FeedItemActionedEvent extends FeedEvent { /// If both are provided, [postId] will take precedence final int? postId; + final List? postIds; + /// This indicates the relevant action to perform on the post final PostAction postAction; @@ -80,7 +82,7 @@ final class FeedItemActionedEvent extends FeedEvent { /// TODO: Change the dynamic type to the correct type(s) if possible final dynamic value; - const FeedItemActionedEvent({this.postViewMedia, this.postId, required this.postAction, this.value}); + const FeedItemActionedEvent({this.postViewMedia, this.postId, this.postIds, required this.postAction, this.value}); } final class FeedClearMessageEvent extends FeedEvent {} diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index 0c58be0b1..ee45d9a30 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -70,18 +70,26 @@ class FeedPostList extends StatelessWidget { context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); }, onUpAction: () { - int past = tabletMode ? 10 : 5; - if (isUserLoggedIn && index > past + 1) { + // Past count tested on multiple devices to ensure posts marked read are above the 0 point + // Reducing this will cause elements still on the screen to be marked read + int past = 6; + if (tabletMode && thunderState.useCompactView) { + past = 22; + } else if (tabletMode && !thunderState.useCompactView) { + past = 12; + } else if (!tabletMode && thunderState.useCompactView) { + past = 11; + } + if (isUserLoggedIn && index > past) { + List markRead = []; for (var i = 0; i < index - past; i++) { if (postViewMedias[i].postView.read != true) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[i].postView.post.id, postAction: PostAction.read, value: true)); - // Debug - bool read = postViewMedias[i].postView.read; - int postId = postViewMedias[i].postView.post.id; - print("marked read $i $read $postId"); - // /Debug + markRead.add(postViewMedias[i].postView.post.id); } } + if (markRead.length > 0) { + context.read().add(FeedItemActionedEvent(postIds: markRead, postAction: PostAction.multiRead, value: true)); + } } }, listingType: state.postListingType, diff --git a/lib/post/enums/post_action.dart b/lib/post/enums/post_action.dart index 8a7e76f4c..87c66f413 100644 --- a/lib/post/enums/post_action.dart +++ b/lib/post/enums/post_action.dart @@ -7,6 +7,7 @@ enum PostAction { delete(permissionType: PermissionType.user), report(permissionType: PermissionType.user), read(permissionType: PermissionType.user), + multiRead(permissionType: PermissionType.user), /// Moderator level post actions lock(permissionType: PermissionType.moderator), diff --git a/lib/post/utils/post.dart b/lib/post/utils/post.dart index b0493b130..ecbe22ce3 100644 --- a/lib/post/utils/post.dart +++ b/lib/post/utils/post.dart @@ -39,6 +39,29 @@ Future markPostAsRead(int postId, bool read) async { return markPostAsReadResponse.isSuccess(); } +/// Logic to mark multiple posts as read +Future> markPostsAsRead(List postIds, bool read) async { + Account? account = await fetchActiveProfileAccount(); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + + if (account?.jwt == null) throw Exception('User not logged in'); + + List failed = []; + // TODO: Lemmy 0.19 support postIds in MarkPostAsRead + // Check what version the server is, and use that if we can + for (int i = 0; i < postIds.length; i++) { + MarkPostAsReadResponse markPostAsReadResponse = await lemmy.run(MarkPostAsRead( + auth: account!.jwt!, + postId: postIds[i], + read: read, + )); + if (!markPostAsReadResponse.isSuccess()) { + failed.add(i); + } + } + return failed; +} + /// Logic to delete post Future deletePost(int postId, bool delete) async { Account? account = await fetchActiveProfileAccount(); From 74ea957da4cc25b23dfc3ccb8de8ad59fda553ad Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 19 Feb 2024 10:35:51 -0500 Subject: [PATCH 07/14] add modified folders to volume mount --- scripts/docker-build-android.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/docker-build-android.sh b/scripts/docker-build-android.sh index 06c7fb81e..5590efd85 100755 --- a/scripts/docker-build-android.sh +++ b/scripts/docker-build-android.sh @@ -14,11 +14,12 @@ docker build \ . # Check docker build folder -mkdir -p ./build/docker +mkdir -p ./build/docker/flutter/gradle/build +mkdir -p ./build/docker/flutter/gradle/.gradle # Create keystore if [ ! -f ./android/app/keystore.jks ]; then - docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder keytool -genkey -v -keystore ./android/app/keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias thunderdev -keypass password -storepass password -srcstorepass password -noprompt -dname "cn=First Last, ou=Java, o=Oracle, c=US" + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -v "${PWD}/build/docker/flutter/gradle/build:/opt/flutter/packages/flutter_tools/gradle/build" -v "${PWD}/build/docker/flutter/gradle/.gradle:/opt/flutter/packages/flutter_tools/gradle/.gradle" -w "${PWD}" thunder-builder keytool -genkey -v -keystore ./android/app/keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias thunderdev -keypass password -storepass password -srcstorepass password -noprompt -dname "cn=First Last, ou=Java, o=Oracle, c=US" fi # Make key properties @@ -34,9 +35,9 @@ fi # Build the APK if [ ! -d ./build/docker/.pub-cache ]; then - docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter pub get' - docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'flutter --disable-analytics' - docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'dart --disable-analytics' + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -v "${PWD}/build/docker/flutter/gradle/build:/opt/flutter/packages/flutter_tools/gradle/build" -v "${PWD}/build/docker/flutter/gradle/.gradle:/opt/flutter/packages/flutter_tools/gradle/.gradle" -w "${PWD}" thunder-builder bash -ic 'flutter pub get' + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -v "${PWD}/build/docker/flutter/gradle/build:/opt/flutter/packages/flutter_tools/gradle/build" -v "${PWD}/build/docker/flutter/gradle/.gradle:/opt/flutter/packages/flutter_tools/gradle/.gradle" -w "${PWD}" thunder-builder bash -ic 'flutter --disable-analytics' + docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -v "${PWD}/build/docker/flutter/gradle/build:/opt/flutter/packages/flutter_tools/gradle/build" -v "${PWD}/build/docker/flutter/gradle/.gradle:/opt/flutter/packages/flutter_tools/gradle/.gradle" -w "${PWD}" thunder-builder bash -ic 'dart --disable-analytics' fi # Create env @@ -53,4 +54,4 @@ export GRADLE_USER_HOME=${HOME} fi # Build the APK -docker run --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -w "${PWD}" thunder-builder bash -ic 'dart scripts/build-android.dart' +docker run --privileged --rm -ti --user "$(id -u)" -e "HOME=/home/builder" --name thunder-builder -v "${PWD}:${PWD}" -v "${PWD}/build/docker:/home/builder" -v "${PWD}/build/docker/flutter/gradle/build:/opt/flutter/packages/flutter_tools/gradle/build" -v "${PWD}/build/docker/flutter/gradle/.gradle:/opt/flutter/packages/flutter_tools/gradle/.gradle" -w "${PWD}" thunder-builder bash -ic 'dart scripts/build-android.dart' From bdb25f09b65de512c5c71e8d6c0093d24747e99b Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 19 Feb 2024 10:42:58 -0500 Subject: [PATCH 08/14] specify flutter version in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc49ee48..17ad219cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: - uses: subosito/flutter-action@v2 with: + flutter_version: "3.16.9" channel: "stable" cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' # optional, change this to force refresh cache From 23dba2fe2c674adb4f569777f971fdb9a5d95e0d Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 19 Feb 2024 11:15:11 -0500 Subject: [PATCH 09/14] add toggle option to settings --- lib/core/enums/local_settings.dart | 3 ++ lib/feed/view/feed_page.dart | 2 + lib/feed/view/feed_widget.dart | 40 ++++++++++--------- lib/l10n/app_en.arb | 4 ++ lib/search/pages/search_page.dart | 2 +- lib/settings/pages/general_settings_page.dart | 18 +++++++++ lib/thunder/bloc/thunder_bloc.dart | 2 + lib/thunder/bloc/thunder_state.dart | 5 +++ 8 files changed, 57 insertions(+), 19 deletions(-) diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 58c0dc458..014eaf6d7 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -90,6 +90,8 @@ enum LocalSettings { useDisplayNamesForUsers(name: 'setting_use_display_names_for_users', key: 'showUserDisplayNames', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.general), markPostAsReadOnMediaView( name: 'setting_general_mark_post_read_on_media_view', key: 'markPostAsReadOnMediaView', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), + markPostAsReadOnScroll( + name: 'setting_general_mark_post_read_on_scroll', key: 'markPostAsReadOnScroll', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), showInAppUpdateNotification( name: 'setting_notifications_show_inapp_update', key: 'showInAppUpdateNotifications', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.notifications), scoreCounters(name: 'setting_score_counters', key: "showScoreCounters", category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), @@ -270,6 +272,7 @@ extension LocalizationExt on AppLocalizations { 'openLinksInReaderMode': openLinksInReaderMode, 'showUserDisplayNames': showUserDisplayNames, 'markPostAsReadOnMediaView': markPostAsReadOnMediaView, + 'markPostAsReadOnScroll': markPostAsReadOnScroll, 'showInAppUpdateNotifications': showInAppUpdateNotifications, 'enableInboxNotifications': enableInboxNotifications, 'showScoreCounters': showScoreCounters, diff --git a/lib/feed/view/feed_page.dart b/lib/feed/view/feed_page.dart index 7de4dd00f..56bfde4b1 100644 --- a/lib/feed/view/feed_page.dart +++ b/lib/feed/view/feed_page.dart @@ -229,6 +229,7 @@ class _FeedViewState extends State { final l10n = AppLocalizations.of(context)!; bool tabletMode = thunderBloc.state.tabletMode; + bool markPostReadOnScroll = thunderBloc.state.markPostReadOnScroll; bool hideTopBarOnScroll = thunderBloc.state.hideTopBarOnScroll; return MultiBlocListener( @@ -339,6 +340,7 @@ class _FeedViewState extends State { FeedPostList( postViewMedias: postViewMedias, tabletMode: tabletMode, + markPostReadOnScroll: markPostReadOnScroll, queuedForRemoval: queuedForRemoval, ), // Widgets to display on the feed when feedType == FeedType.community diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index ee45d9a30..155f34770 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -11,6 +11,7 @@ import 'package:thunder/thunder/bloc/thunder_bloc.dart'; class FeedPostList extends StatelessWidget { final bool tabletMode; + final bool markPostReadOnScroll; final List? queuedForRemoval; final List postViewMedias; @@ -18,6 +19,7 @@ class FeedPostList extends StatelessWidget { super.key, required this.postViewMedias, required this.tabletMode, + required this.markPostReadOnScroll, this.queuedForRemoval, }); @@ -70,25 +72,27 @@ class FeedPostList extends StatelessWidget { context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); }, onUpAction: () { - // Past count tested on multiple devices to ensure posts marked read are above the 0 point - // Reducing this will cause elements still on the screen to be marked read - int past = 6; - if (tabletMode && thunderState.useCompactView) { - past = 22; - } else if (tabletMode && !thunderState.useCompactView) { - past = 12; - } else if (!tabletMode && thunderState.useCompactView) { - past = 11; - } - if (isUserLoggedIn && index > past) { - List markRead = []; - for (var i = 0; i < index - past; i++) { - if (postViewMedias[i].postView.read != true) { - markRead.add(postViewMedias[i].postView.post.id); - } + if (markPostReadOnScroll) { + // Past count tested on multiple devices to ensure posts marked read are above the 0 point + // Reducing this will cause elements still on the screen to be marked read + int past = 6; + if (tabletMode && thunderState.useCompactView) { + past = 22; + } else if (tabletMode && !thunderState.useCompactView) { + past = 12; + } else if (!tabletMode && thunderState.useCompactView) { + past = 11; } - if (markRead.length > 0) { - context.read().add(FeedItemActionedEvent(postIds: markRead, postAction: PostAction.multiRead, value: true)); + if (isUserLoggedIn && index > past) { + List markRead = []; + for (var i = 0; i < index - past; i++) { + if (postViewMedias[i].postView.read != true) { + markRead.add(postViewMedias[i].postView.post.id); + } + } + if (markRead.length > 0) { + context.read().add(FeedItemActionedEvent(postIds: markRead, postAction: PostAction.multiRead, value: true)); + } } } }, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 133ab6c4d..52ff50a62 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -715,6 +715,10 @@ "@markPostAsReadOnMediaView": { "description": "Toggle to mark posts as read after viewing media." }, + "markPostAsReadOnScroll": "Mark Read On Scroll", + "@markPostAsReadOnScroll": { + "description": "Toggle to mark posts as read as you scroll past them in the feed." + }, "medium": "Medium", "@medium": { "description": "Description for medium font scale" diff --git a/lib/search/pages/search_page.dart b/lib/search/pages/search_page.dart index a719e19d1..048f5006b 100644 --- a/lib/search/pages/search_page.dart +++ b/lib/search/pages/search_page.dart @@ -689,7 +689,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi child: CustomScrollView( controller: _scrollController, slivers: [ - FeedPostList(postViewMedias: state.posts ?? [], tabletMode: tabletMode), + FeedPostList(postViewMedias: state.posts ?? [], tabletMode: tabletMode, markPostReadOnScroll: false), if (state.status == SearchStatus.refreshing) const SliverToBoxAdapter( child: Center( diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index f2ce0c674..e11c4566f 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -71,6 +71,9 @@ class _GeneralSettingsPageState extends State with SingleTi /// When enabled, posts will be marked as read when opening the image/media bool markPostReadOnMediaView = false; + /// When enabled, posts will be marked as read when scrolling + bool markPostReadOnScroll = false; + /// When enabled, the top bar will be hidden on scroll bool hideTopBarOnScroll = false; @@ -148,6 +151,10 @@ class _GeneralSettingsPageState extends State with SingleTi await prefs.setBool(LocalSettings.markPostAsReadOnMediaView.name, value); setState(() => markPostReadOnMediaView = value); break; + case LocalSettings.markPostAsReadOnScroll: + await prefs.setBool(LocalSettings.markPostAsReadOnScroll.name, value); + setState(() => markPostReadOnScroll = value); + break; case LocalSettings.useTabletMode: await prefs.setBool(LocalSettings.useTabletMode.name, value); setState(() => tabletMode = value); @@ -235,6 +242,7 @@ class _GeneralSettingsPageState extends State with SingleTi hideNsfwPosts = prefs.getBool(LocalSettings.hideNsfwPosts.name) ?? false; tappableAuthorCommunity = prefs.getBool(LocalSettings.tappableAuthorCommunity.name) ?? false; markPostReadOnMediaView = prefs.getBool(LocalSettings.markPostAsReadOnMediaView.name) ?? false; + markPostReadOnScroll = prefs.getBool(LocalSettings.markPostAsReadOnScroll.name) ?? false; tabletMode = prefs.getBool(LocalSettings.useTabletMode.name) ?? false; hideTopBarOnScroll = prefs.getBool(LocalSettings.hideTopBarOnScroll.name) ?? false; @@ -451,6 +459,16 @@ class _GeneralSettingsPageState extends State with SingleTi highlightKey: settingToHighlight == LocalSettings.markPostAsReadOnMediaView ? settingToHighlightKey : null, ), ), + SliverToBoxAdapter( + child: ToggleOption( + description: l10n.markPostAsReadOnScroll, + value: markPostReadOnScroll, + iconEnabled: Icons.playlist_add_check, + iconDisabled: Icons.playlist_add, + onToggle: (bool value) => setPreferences(LocalSettings.markPostAsReadOnScroll, value), + highlightKey: settingToHighlight == LocalSettings.markPostAsReadOnScroll ? settingToHighlightKey : null, + ), + ), SliverToBoxAdapter( child: ToggleOption( description: l10n.tabletMode, diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index 4a0530214..7ea162504 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -112,6 +112,7 @@ class ThunderBloc extends Bloc { bool openInReaderMode = prefs.getBool(LocalSettings.openLinksInReaderMode.name) ?? false; bool useDisplayNames = prefs.getBool(LocalSettings.useDisplayNamesForUsers.name) ?? true; bool markPostReadOnMediaView = prefs.getBool(LocalSettings.markPostAsReadOnMediaView.name) ?? false; + bool markPostReadOnScroll = prefs.getBool(LocalSettings.markPostAsReadOnScroll.name) ?? false; bool showInAppUpdateNotification = prefs.getBool(LocalSettings.showInAppUpdateNotification.name) ?? false; bool enableInboxNotifications = prefs.getBool(LocalSettings.enableInboxNotifications.name) ?? false; String? appLanguageCode = prefs.getString(LocalSettings.appLanguageCode.name) ?? 'en'; @@ -250,6 +251,7 @@ class ThunderBloc extends Bloc { openInReaderMode: openInReaderMode, useDisplayNames: useDisplayNames, markPostReadOnMediaView: markPostReadOnMediaView, + markPostReadOnScroll: markPostReadOnScroll, showInAppUpdateNotification: showInAppUpdateNotification, enableInboxNotifications: enableInboxNotifications, appLanguageCode: appLanguageCode, diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index 6a30c2d9e..83a20d27e 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -28,6 +28,7 @@ class ThunderState extends Equatable { this.openInReaderMode = false, this.useDisplayNames = true, this.markPostReadOnMediaView = false, + this.markPostReadOnScroll = false, this.disableFeedFab = false, this.showInAppUpdateNotification = false, this.enableInboxNotifications = false, @@ -160,6 +161,7 @@ class ThunderState extends Equatable { final bool openInReaderMode; final bool useDisplayNames; final bool markPostReadOnMediaView; + final bool markPostReadOnScroll; final bool disableFeedFab; final bool showInAppUpdateNotification; final bool enableInboxNotifications; @@ -301,6 +303,7 @@ class ThunderState extends Equatable { bool? openInReaderMode, bool? useDisplayNames, bool? markPostReadOnMediaView, + bool? markPostReadOnScroll, bool? showInAppUpdateNotification, bool? enableInboxNotifications, bool? scoreCounters, @@ -432,6 +435,7 @@ class ThunderState extends Equatable { openInReaderMode: openInReaderMode ?? this.openInReaderMode, useDisplayNames: useDisplayNames ?? this.useDisplayNames, markPostReadOnMediaView: markPostReadOnMediaView ?? this.markPostReadOnMediaView, + markPostReadOnScroll: markPostReadOnScroll ?? this.markPostReadOnScroll, disableFeedFab: disableFeedFab, showInAppUpdateNotification: showInAppUpdateNotification ?? this.showInAppUpdateNotification, enableInboxNotifications: enableInboxNotifications ?? this.enableInboxNotifications, @@ -572,6 +576,7 @@ class ThunderState extends Equatable { browserMode, useDisplayNames, markPostReadOnMediaView, + markPostReadOnScroll, disableFeedFab, showInAppUpdateNotification, enableInboxNotifications, From bfec3984169d458e371e46d8dbd9d471372e10c8 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 19 Feb 2024 11:18:01 -0500 Subject: [PATCH 10/14] update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c62b6cfd..859157439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ - Added additional font scale option for medium - Added ability to subscribe/unsubscribe to community from long press action on posts - Added option to hide top app bar on scroll -- Ability to search through settings/preferences contribution from @ggichure. +- Ability to search through settings/preferences - contribution from @ggichure - Setting to use colorized usernames - contribution from @ggichure. - Show the number of new comments a read post has received since last visited +- Added ability to mark read on scroll - contribution from @Fmstrat ## Changed - Small UI adjustments for account switcher From 1ffaa9197648e187075c57d20352a77b125ebc0b Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sat, 24 Feb 2024 09:31:04 -0500 Subject: [PATCH 11/14] switch to visibility detector --- android/build.gradle | 4 +- lib/community/widgets/post_card.dart | 6 +- lib/community/widgets/post_card_list.dart | 1 + lib/feed/view/feed_widget.dart | 79 ++++++++++++----------- pubspec.lock | 8 +++ pubspec.yaml | 1 + 6 files changed, 60 insertions(+), 39 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 714812d9d..6cca680a5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,8 +3,8 @@ buildscript { // Versions recommended/needed by flutter_local_notifications and background_fetch // These can be upgraded over time. ext { - compileSdkVersion = 33 - targetSdkVersion = 33 + compileSdkVersion = 34 + targetSdkVersion = 34 appCompatVersion = "1.6.1" } repositories { diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index 5efa508b2..e8278ad6f 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -25,6 +25,7 @@ class PostCard extends StatefulWidget { final Function(bool) onSaveAction; final Function(bool) onReadAction; final Function() onUpAction; + final Function() onDownAction; final ListingType? listingType; @@ -36,6 +37,7 @@ class PostCard extends StatefulWidget { required this.onSaveAction, required this.onReadAction, required this.onUpAction, + required this.onDownAction, required this.listingType, required this.indicateRead, }); @@ -83,7 +85,9 @@ class _PostCardState extends State { return Listener( behavior: HitTestBehavior.opaque, - onPointerDown: (event) => {}, + onPointerDown: (event) { + widget.onDownAction(); + }, onPointerUp: (event) { setState(() => isOverridingSwipeGestureAction = false); diff --git a/lib/community/widgets/post_card_list.dart b/lib/community/widgets/post_card_list.dart index 9fd4ab163..89c78233a 100644 --- a/lib/community/widgets/post_card_list.dart +++ b/lib/community/widgets/post_card_list.dart @@ -166,6 +166,7 @@ class _PostCardListState extends State { onSaveAction: (bool saved) => widget.onSaveAction(postViewMedia.postView.post.id, saved), onReadAction: (bool read) => widget.onToggleReadAction(postViewMedia.postView.post.id, read), onUpAction: () {}, + onDownAction: () {}, listingType: widget.listingType, indicateRead: widget.indicateRead, ); diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index 155f34770..def833ffd 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -8,14 +8,18 @@ import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class FeedPostList extends StatelessWidget { final bool tabletMode; final bool markPostReadOnScroll; final List? queuedForRemoval; final List postViewMedias; + int prevLastTappedIndex = -1; + int lastTappedIndex = 0; + List markReadPostIds = []; - const FeedPostList({ + FeedPostList({ super.key, required this.postViewMedias, required this.tabletMode, @@ -28,6 +32,7 @@ class FeedPostList extends StatelessWidget { final ThunderState thunderState = context.read().state; final FeedState state = context.read().state; final bool isUserLoggedIn = context.read().state.isLoggedIn; + VisibilityDetectorController.instance.updateInterval = Duration.zero; // Widget representing the list of posts on the feed return SliverMasonryGrid.count( @@ -59,45 +64,47 @@ class FeedPostList extends StatelessWidget { ); }, child: queuedForRemoval?.contains(postViewMedias[index].postView.post.id) != true - ? PostCard( - postViewMedia: postViewMedias[index], - communityMode: state.feedType == FeedType.community, - onVoteAction: (int voteType) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType)); - }, - onSaveAction: (bool saved) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved)); - }, - onReadAction: (bool read) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); - }, - onUpAction: () { - if (markPostReadOnScroll) { - // Past count tested on multiple devices to ensure posts marked read are above the 0 point - // Reducing this will cause elements still on the screen to be marked read - int past = 6; - if (tabletMode && thunderState.useCompactView) { - past = 22; - } else if (tabletMode && !thunderState.useCompactView) { - past = 12; - } else if (!tabletMode && thunderState.useCompactView) { - past = 11; - } - if (isUserLoggedIn && index > past) { - List markRead = []; - for (var i = 0; i < index - past; i++) { - if (postViewMedias[i].postView.read != true) { - markRead.add(postViewMedias[i].postView.post.id); - } - } - if (markRead.length > 0) { - context.read().add(FeedItemActionedEvent(postIds: markRead, postAction: PostAction.multiRead, value: true)); + ? VisibilityDetector( + key: Key('post-card-vis-' + index.toString()), + onVisibilityChanged: (info) { + if (markPostReadOnScroll && isUserLoggedIn && index <= lastTappedIndex && postViewMedias[index].postView.read != true + && lastTappedIndex > prevLastTappedIndex && info.visibleFraction < .25 && !markReadPostIds.contains(postViewMedias[index].postView.post.id)) { + // Sometimes the event doesn't fire, so check all previous indexes up to the last one marked unread + List toAdd = [postViewMedias[index].postView.post.id]; + for (int i = index - 1; i >= 0; i--) { + if (postViewMedias[i].postView.read) break; + if (!markReadPostIds.contains(postViewMedias[index].postView.post.id)) { + toAdd.add(postViewMedias[i].postView.post.id); } } + markReadPostIds = [...markReadPostIds, ...toAdd]; } }, - listingType: state.postListingType, - indicateRead: thunderState.dimReadPosts, + child: PostCard( + postViewMedia: postViewMedias[index], + communityMode: state.feedType == FeedType.community, + onVoteAction: (int voteType) { + context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType)); + }, + onSaveAction: (bool saved) { + context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved)); + }, + onReadAction: (bool read) { + context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); + }, + onDownAction: () { + prevLastTappedIndex = lastTappedIndex; + lastTappedIndex = index; + }, + onUpAction: () { + if (markPostReadOnScroll && markReadPostIds.length > 0) { + context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true)); + markReadPostIds = []; + } + }, + listingType: state.postListingType, + indicateRead: thunderState.dimReadPosts, + ) ) : null, ); diff --git a/pubspec.lock b/pubspec.lock index b41f001e3..682a976d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1743,6 +1743,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f994cb548..72efea763 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -108,6 +108,7 @@ dependencies: background_fetch: ^1.2.1 gal: ^2.2.0 smooth_highlight: ^0.1.1 + visibility_detector: ^0.4.0+2 dev_dependencies: build_runner: ^2.4.6 From 30b49c87363e3816a80789db7d6c81bc0d7ccc0d Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sat, 24 Feb 2024 09:34:59 -0500 Subject: [PATCH 12/14] updated linting --- lib/core/enums/local_settings.dart | 3 +-- lib/feed/view/feed_widget.dart | 12 ++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 014eaf6d7..ad2dbe87c 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -90,8 +90,7 @@ enum LocalSettings { useDisplayNamesForUsers(name: 'setting_use_display_names_for_users', key: 'showUserDisplayNames', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.general), markPostAsReadOnMediaView( name: 'setting_general_mark_post_read_on_media_view', key: 'markPostAsReadOnMediaView', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), - markPostAsReadOnScroll( - name: 'setting_general_mark_post_read_on_scroll', key: 'markPostAsReadOnScroll', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), + markPostAsReadOnScroll(name: 'setting_general_mark_post_read_on_scroll', key: 'markPostAsReadOnScroll', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), showInAppUpdateNotification( name: 'setting_notifications_show_inapp_update', key: 'showInAppUpdateNotifications', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.notifications), scoreCounters(name: 'setting_score_counters', key: "showScoreCounters", category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index def833ffd..a0d254332 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -67,8 +67,13 @@ class FeedPostList extends StatelessWidget { ? VisibilityDetector( key: Key('post-card-vis-' + index.toString()), onVisibilityChanged: (info) { - if (markPostReadOnScroll && isUserLoggedIn && index <= lastTappedIndex && postViewMedias[index].postView.read != true - && lastTappedIndex > prevLastTappedIndex && info.visibleFraction < .25 && !markReadPostIds.contains(postViewMedias[index].postView.post.id)) { + if (markPostReadOnScroll && + isUserLoggedIn && + index <= lastTappedIndex && + postViewMedias[index].postView.read != true && + lastTappedIndex > prevLastTappedIndex && + info.visibleFraction < .25 && + !markReadPostIds.contains(postViewMedias[index].postView.post.id)) { // Sometimes the event doesn't fire, so check all previous indexes up to the last one marked unread List toAdd = [postViewMedias[index].postView.post.id]; for (int i = index - 1; i >= 0; i--) { @@ -104,8 +109,7 @@ class FeedPostList extends StatelessWidget { }, listingType: state.postListingType, indicateRead: thunderState.dimReadPosts, - ) - ) + )) : null, ); }, From 581f7a1b937d3c801bbc2a2d4540a7c0a46d3996 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Mon, 26 Feb 2024 08:01:09 -0500 Subject: [PATCH 13/14] only mark on upward scroll --- lib/community/widgets/post_card.dart | 10 ++++++++-- lib/community/widgets/post_card_list.dart | 2 +- lib/feed/view/feed_widget.dart | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index e8278ad6f..81088ce82 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -24,7 +24,7 @@ class PostCard extends StatefulWidget { final Function(int) onVoteAction; final Function(bool) onSaveAction; final Function(bool) onReadAction; - final Function() onUpAction; + final Function(double) onUpAction; final Function() onDownAction; final ListingType? listingType; @@ -68,6 +68,9 @@ class _PostCardState extends State { /// This is used to temporarily disable the swipe action to allow for detection of full screen swipe to go back bool isOverridingSwipeGestureAction = false; + /// The vertical drag distance between moves + double verticalDragDistance = 0; + @override void initState() { super.initState(); @@ -105,13 +108,16 @@ class _PostCardState extends State { ); } - widget.onUpAction(); + widget.onUpAction(verticalDragDistance); }, onPointerCancel: (event) => {}, onPointerMove: (PointerMoveEvent event) { // Get the horizontal drag distance double horizontalDragDistance = event.delta.dx; + // Set the vertical drag distance + verticalDragDistance = event.delta.dy; + // We are checking to see if there is a left to right swipe here. If there is a left to right swipe, and LTR swipe actions are disabled, then we disable the DismissDirection temporarily // to allow for the full screen swipe to go back. Otherwise, we retain the default behaviour if (horizontalDragDistance > 0) { diff --git a/lib/community/widgets/post_card_list.dart b/lib/community/widgets/post_card_list.dart index 89c78233a..d98c0b285 100644 --- a/lib/community/widgets/post_card_list.dart +++ b/lib/community/widgets/post_card_list.dart @@ -165,7 +165,7 @@ class _PostCardListState extends State { onVoteAction: (int voteType) => widget.onVoteAction(postViewMedia.postView.post.id, voteType), onSaveAction: (bool saved) => widget.onSaveAction(postViewMedia.postView.post.id, saved), onReadAction: (bool read) => widget.onToggleReadAction(postViewMedia.postView.post.id, read), - onUpAction: () {}, + onUpAction: (double verticalDragDistance) {}, onDownAction: () {}, listingType: widget.listingType, indicateRead: widget.indicateRead, diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index a0d254332..0c8418e7d 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -72,7 +72,7 @@ class FeedPostList extends StatelessWidget { index <= lastTappedIndex && postViewMedias[index].postView.read != true && lastTappedIndex > prevLastTappedIndex && - info.visibleFraction < .25 && + info.visibleFraction <= 0 && !markReadPostIds.contains(postViewMedias[index].postView.post.id)) { // Sometimes the event doesn't fire, so check all previous indexes up to the last one marked unread List toAdd = [postViewMedias[index].postView.post.id]; @@ -101,8 +101,8 @@ class FeedPostList extends StatelessWidget { prevLastTappedIndex = lastTappedIndex; lastTappedIndex = index; }, - onUpAction: () { - if (markPostReadOnScroll && markReadPostIds.length > 0) { + onUpAction: (double verticalDragDistance) { + if (markPostReadOnScroll && verticalDragDistance < 0 && markReadPostIds.length > 0) { context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true)); markReadPostIds = []; } From fcc1737ff869e54c7439b1bc8164bfc44f921d89 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 3 Mar 2024 17:34:24 -0800 Subject: [PATCH 14/14] adjusted the logic for determining read posts --- lib/community/widgets/post_card.dart | 2 +- lib/core/singletons/lemmy_client.dart | 3 +- lib/feed/bloc/feed_bloc.dart | 10 +-- lib/feed/view/feed_widget.dart | 112 ++++++++++++++++++-------- lib/instance/pages/instance_page.dart | 1 + lib/post/utils/post.dart | 22 +++-- 6 files changed, 103 insertions(+), 47 deletions(-) diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index 81088ce82..fd60f4e70 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -88,7 +88,7 @@ class _PostCardState extends State { return Listener( behavior: HitTestBehavior.opaque, - onPointerDown: (event) { + onPointerDown: (PointerDownEvent event) { widget.onDownAction(); }, onPointerUp: (event) { diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index 71dc402c6..c22212bc7 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -55,7 +55,8 @@ enum LemmyFeature { sortTypeControversial(0, 19, 0, preRelease: ["rc", "1"]), sortTypeScaled(0, 19, 0, preRelease: ["rc", "1"]), commentSortTypeControversial(0, 19, 0, preRelease: ["rc", "1"]), - blockInstance(0, 19, 0, preRelease: ["rc", "1"]); + blockInstance(0, 19, 0, preRelease: ["rc", "1"]), + multiRead(0, 19, 0, preRelease: ["rc", "1"]); final int major; final int minor; diff --git a/lib/feed/bloc/feed_bloc.dart b/lib/feed/bloc/feed_bloc.dart index 1048aae36..fc5cb29db 100644 --- a/lib/feed/bloc/feed_bloc.dart +++ b/lib/feed/bloc/feed_bloc.dart @@ -126,7 +126,7 @@ class FeedBloc extends Bloc { /// Handles post related actions on a given item within the feed Future _onFeedItemActioned(FeedItemActionedEvent event, Emitter emit) async { - assert(!(event.postViewMedia == null && event.postId == null)); + assert(!(event.postViewMedia == null && event.postId == null && event.postIds == null)); emit(state.copyWith(status: FeedStatus.fetching)); // TODO: Check if the current account has permission to perform the PostAction @@ -210,7 +210,8 @@ class FeedBloc extends Bloc { } case PostAction.multiRead: List eventPostIds = event.postIds ?? []; - if (eventPostIds.length > 0) { + + if (eventPostIds.isNotEmpty) { // Optimistically read the posts List existingPostViewMediaIndexes = []; List postIds = []; @@ -225,9 +226,6 @@ class FeedBloc extends Bloc { } } - // Give a slight delay to have the UI perform any navigation first - await Future.delayed(const Duration(milliseconds: 250)); - try { for (int i = 0; i < existingPostViewMediaIndexes.length; i++) { PostView updatedPostView = optimisticallyReadPost(postViewMedias[i], event.value); @@ -239,7 +237,7 @@ class FeedBloc extends Bloc { emit(state.copyWith(status: FeedStatus.fetching)); List failed = await markPostsAsRead(postIds, event.value); - if (failed.length == 0) return emit(state.copyWith(status: FeedStatus.success)); + if (failed.isEmpty) return emit(state.copyWith(status: FeedStatus.success)); // Restore the original post contents if not successful for (int i = 0; i < failed.length; i++) { diff --git a/lib/feed/view/feed_widget.dart b/lib/feed/view/feed_widget.dart index 0c8418e7d..12293dfc5 100644 --- a/lib/feed/view/feed_widget.dart +++ b/lib/feed/view/feed_widget.dart @@ -1,6 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + import 'package:thunder/community/widgets/post_card.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/post_view_media.dart'; @@ -8,18 +13,21 @@ import 'package:thunder/feed/bloc/feed_bloc.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; -import 'package:visibility_detector/visibility_detector.dart'; -class FeedPostList extends StatelessWidget { +class FeedPostList extends StatefulWidget { + /// Determines the number of columns to display final bool tabletMode; + + /// Determines whether to mark posts as read on scroll final bool markPostReadOnScroll; + + /// The list of posts that have been queued for removal using the dismiss read action final List? queuedForRemoval; + + /// The list of posts to show on the feed final List postViewMedias; - int prevLastTappedIndex = -1; - int lastTappedIndex = 0; - List markReadPostIds = []; - FeedPostList({ + const FeedPostList({ super.key, required this.postViewMedias, required this.tabletMode, @@ -27,16 +35,43 @@ class FeedPostList extends StatelessWidget { this.queuedForRemoval, }); + @override + State createState() => _FeedPostListState(); +} + +class _FeedPostListState extends State { + /// The index of the last tapped post. + /// This is used to calculate the read status of posts in the range [0, lastTappedIndex] + int lastTappedIndex = -1; + + /// Whether the user is scrolling down or not. The logic for determining read posts will + /// only be applied when the user is scrolling down + bool isScrollingDown = false; + + /// List of post ids to queue for being marked as read. + Set markReadPostIds = {}; + + /// List of post ids that have already previously been detected as read + Set readPostIds = {}; + + /// Timer for debouncing the read action + Timer? debounceTimer; + + @override + void dispose() { + debounceTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final ThunderState thunderState = context.read().state; final FeedState state = context.read().state; final bool isUserLoggedIn = context.read().state.isLoggedIn; - VisibilityDetectorController.instance.updateInterval = Duration.zero; // Widget representing the list of posts on the feed return SliverMasonryGrid.count( - crossAxisCount: tabletMode ? 2 : 1, + crossAxisCount: widget.tabletMode ? 2 : 1, crossAxisSpacing: 40, mainAxisSpacing: 0, itemBuilder: (BuildContext context, int index) { @@ -63,48 +98,57 @@ class FeedPostList extends StatelessWidget { ), ); }, - child: queuedForRemoval?.contains(postViewMedias[index].postView.post.id) != true + child: widget.queuedForRemoval?.contains(widget.postViewMedias[index].postView.post.id) != true ? VisibilityDetector( - key: Key('post-card-vis-' + index.toString()), + key: Key('post-card-vis-$index'), onVisibilityChanged: (info) { - if (markPostReadOnScroll && - isUserLoggedIn && - index <= lastTappedIndex && - postViewMedias[index].postView.read != true && - lastTappedIndex > prevLastTappedIndex && - info.visibleFraction <= 0 && - !markReadPostIds.contains(postViewMedias[index].postView.post.id)) { - // Sometimes the event doesn't fire, so check all previous indexes up to the last one marked unread - List toAdd = [postViewMedias[index].postView.post.id]; - for (int i = index - 1; i >= 0; i--) { - if (postViewMedias[i].postView.read) break; - if (!markReadPostIds.contains(postViewMedias[index].postView.post.id)) { - toAdd.add(postViewMedias[i].postView.post.id); + if (!isUserLoggedIn || !widget.markPostReadOnScroll || !isScrollingDown) return; + + if (index <= lastTappedIndex && info.visibleFraction == 0) { + for (int i = index; i >= 0; i--) { + // If we already checked this post's read status, or we already marked it as read, skip it + if (readPostIds.contains(widget.postViewMedias[i].postView.post.id)) continue; + if (markReadPostIds.contains(widget.postViewMedias[i].postView.post.id)) continue; + + // Otherwise, check the post read status + if (widget.postViewMedias[i].postView.read == false) { + markReadPostIds.add(widget.postViewMedias[i].postView.post.id); + } else { + readPostIds.add(widget.postViewMedias[i].postView.post.id); } } - markReadPostIds = [...markReadPostIds, ...toAdd]; + + // Debounce the read action to account for quick scrolling. This reduces the number of times the read action is triggered + debounceTimer?.cancel(); + + debounceTimer = Timer(const Duration(milliseconds: 500), () { + if (markReadPostIds.isNotEmpty) { + context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true)); + markReadPostIds = {}; + } + }); } }, child: PostCard( - postViewMedia: postViewMedias[index], + postViewMedia: widget.postViewMedias[index], communityMode: state.feedType == FeedType.community, onVoteAction: (int voteType) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType)); + context.read().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType)); }, onSaveAction: (bool saved) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved)); + context.read().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved)); }, onReadAction: (bool read) { - context.read().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); + context.read().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read)); }, onDownAction: () { - prevLastTappedIndex = lastTappedIndex; - lastTappedIndex = index; + if (lastTappedIndex != index) lastTappedIndex = index; }, onUpAction: (double verticalDragDistance) { - if (markPostReadOnScroll && verticalDragDistance < 0 && markReadPostIds.length > 0) { - context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true)); - markReadPostIds = []; + bool updatedIsScrollingDown = verticalDragDistance < 0; + + if (isScrollingDown != updatedIsScrollingDown) { + isScrollingDown = updatedIsScrollingDown; } }, listingType: state.postListingType, @@ -113,7 +157,7 @@ class FeedPostList extends StatelessWidget { : null, ); }, - childCount: postViewMedias.length, + childCount: widget.postViewMedias.length, ); } } diff --git a/lib/instance/pages/instance_page.dart b/lib/instance/pages/instance_page.dart index f68fd6880..0d6545a0f 100644 --- a/lib/instance/pages/instance_page.dart +++ b/lib/instance/pages/instance_page.dart @@ -285,6 +285,7 @@ class _InstancePageState extends State { ), if (viewType == SearchType.posts) FeedPostList( + markPostReadOnScroll: false, postViewMedias: state.posts ?? [], tabletMode: tabletMode, ), diff --git a/lib/post/utils/post.dart b/lib/post/utils/post.dart index 66360c29c..3f4251fd6 100644 --- a/lib/post/utils/post.dart +++ b/lib/post/utils/post.dart @@ -47,18 +47,30 @@ Future> markPostsAsRead(List postIds, bool read) async { if (account?.jwt == null) throw Exception('User not logged in'); List failed = []; - // TODO: Lemmy 0.19 support postIds in MarkPostAsRead - // Check what version the server is, and use that if we can - for (int i = 0; i < postIds.length; i++) { + + if (LemmyClient.instance.supportsFeature(LemmyFeature.multiRead)) { MarkPostAsReadResponse markPostAsReadResponse = await lemmy.run(MarkPostAsRead( auth: account!.jwt!, - postId: postIds[i], + postIds: postIds, read: read, )); + if (!markPostAsReadResponse.isSuccess()) { - failed.add(i); + failed = List.generate(postIds.length, (index) => index); + } + } else { + for (int i = 0; i < postIds.length; i++) { + MarkPostAsReadResponse markPostAsReadResponse = await lemmy.run(MarkPostAsRead( + auth: account!.jwt!, + postId: postIds[i], + read: read, + )); + if (!markPostAsReadResponse.isSuccess()) { + failed.add(i); + } } } + return failed; }