Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:html/parser.dart';
import 'package:markdown/markdown.dart' hide Text;
import 'package:thunder/src/app/cubits/feed_ui_cubit/feed_ui_cubit.dart';

import 'package:thunder/src/app/cubits/feed_ui_cubit/feed_ui_cubit.dart';
import 'package:thunder/src/features/community/community.dart';
import 'package:thunder/src/features/post/post.dart';
import 'package:thunder/src/core/enums/media_type.dart';
Expand Down Expand Up @@ -204,7 +202,7 @@ class PostCardViewComfortable extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(bottom: 6.0, left: 12.0, right: 12.0),
child: ScalableText(
parse(markdownToHtml(textContent)).documentElement?.text.trim() ?? textContent,
post.textPreview ?? textContent,
maxLines: 4,
overflow: TextOverflow.ellipsis,
fontScale: contentFontSizeScale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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/src/features/post/post.dart';
import 'package:visibility_detector/visibility_detector.dart';

import 'package:thunder/src/features/post/post.dart';
import 'package:thunder/src/features/account/account.dart';
import 'package:thunder/src/core/enums/enums.dart';
import 'package:thunder/src/features/community/community.dart';
Expand Down Expand Up @@ -57,6 +57,9 @@ class _FeedPostCardListState extends State<FeedPostCardList> {
/// This is used to calculate the read status of posts in the range [0, lastTappedIndex]
int lastTappedIndex = -1;

/// The index of the last processed post for read status.
int lastProcessedIndex = -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;
Expand Down Expand Up @@ -134,7 +137,10 @@ class _FeedPostCardListState extends State<FeedPostCardList> {

debounceTimer = Timer(const Duration(milliseconds: 500), () {
// TODO: Improve logic here so that we don't have to iterate through all posts if possible.
for (int i = index; i >= 0; i--) {
int startIndex = index;
int endIndex = lastProcessedIndex > 0 ? lastProcessedIndex : 0;

for (int i = startIndex; i >= endIndex; i--) {
final post = widget.posts[i];

// If we already checked this post's read status, or we already marked it as read, skip it
Expand All @@ -145,6 +151,9 @@ class _FeedPostCardListState extends State<FeedPostCardList> {
readPostIds.add(post.id);
}

// Update the last processed index
if (index > lastProcessedIndex) lastProcessedIndex = index;

if (markReadPostIds.isNotEmpty) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true));
readPostIds.addAll(markReadPostIds); // Add all post ids that were queued to prevent them from being queued again
Expand Down
9 changes: 9 additions & 0 deletions lib/src/features/post/data/models/thunder_post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class ThunderPost extends Equatable {
/// The post's alternate text
final String? altText;

/// The post's text preview.
///
/// This field is not returned by the API, but is computed locally during post parsing.
final String? textPreview;

/// The post's creator
final ThunderUser? creator;

Expand Down Expand Up @@ -163,6 +168,7 @@ class ThunderPost extends Equatable {
this.myVote,
this.unreadComments,
this.media = const [],
this.textPreview,
});

@override
Expand Down Expand Up @@ -206,6 +212,7 @@ class ThunderPost extends Equatable {
myVote,
unreadComments,
media,
textPreview,
];

ThunderPost copyWith({
Expand Down Expand Up @@ -248,6 +255,7 @@ class ThunderPost extends Equatable {
int? myVote,
int? unreadComments,
List<Media>? media,
String? textPreview,
}) {
return ThunderPost(
id: id ?? this.id,
Expand Down Expand Up @@ -289,6 +297,7 @@ class ThunderPost extends Equatable {
myVote: myVote ?? this.myVote,
unreadComments: unreadComments ?? this.unreadComments,
media: media ?? this.media,
textPreview: textPreview ?? this.textPreview,
);
}

Expand Down
15 changes: 12 additions & 3 deletions lib/src/features/post/presentation/utils/post.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';

import 'package:html_unescape/html_unescape_small.dart';
import 'package:html/parser.dart';
import 'package:markdown/markdown.dart' hide Text;

import 'package:thunder/src/features/account/account.dart';
import 'package:thunder/src/core/enums/local_settings.dart';
Expand All @@ -12,6 +14,8 @@ import 'package:thunder/src/features/search/search.dart';
import 'package:thunder/src/shared/utils/media/image.dart';
import 'package:thunder/src/shared/utils/media/video.dart';

final _htmlUnescape = HtmlUnescape();

// Optimistically updates a post. This changes the value of the post locally, without sending the network request
ThunderPost optimisticallyVotePost(ThunderPost post, int voteType) {
int newScore = post.score!;
Expand Down Expand Up @@ -114,8 +118,13 @@ Future<List<ThunderPost>> parsePosts(List<ThunderPost> posts, {String? resolutio
///
/// This includes unescaping the title and parsing any associated media.
Future<ThunderPost> parsePost(ThunderPost post, bool fetchImageDimensions, bool edgeToEdgeImages, bool tabletMode) async {
final html = HtmlUnescape();
final title = html.convert(post.name);
final title = _htmlUnescape.convert(post.name);

// Compute text preview
String? textPreview;
if (post.body != null && post.body!.isNotEmpty) {
textPreview = parse(markdownToHtml(post.body!)).documentElement?.text.trim() ?? post.body;
}

List<Media> mediaList = [];

Expand Down Expand Up @@ -191,5 +200,5 @@ Future<ThunderPost> parsePost(ThunderPost post, bool fetchImageDimensions, bool
media.height = scaledSize?.height;
mediaList.add(media);

return post.copyWith(media: mediaList, name: title);
return post.copyWith(media: mediaList, name: title, textPreview: textPreview);
}
48 changes: 41 additions & 7 deletions lib/src/shared/utils/media/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:flutter/services.dart';

import 'package:flutter_avif/flutter_avif.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart' as http;
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
import 'package:image_dimension_parser/image_dimension_parser.dart';

import 'package:thunder/src/core/cache/image_dimension_cache.dart';
import 'package:thunder/src/shared/images/image_viewer.dart';
Expand Down Expand Up @@ -114,6 +115,25 @@ bool _isAvifImage(String path) {
return path.toLowerCase().endsWith('.avif');
}

/// Fetches the image dimensions from the given URL using partial content fetch
Future<List<int>> processImageDimensions(String imageUrl) async {
try {
final response = await http.get(
Uri.parse(imageUrl),
headers: {'Range': 'bytes=0-10240'}, // 10KB
);

if (response.statusCode == 206 || response.statusCode == 200) {
final sizeResult = ImageDimensionParser().parse(response.bodyBytes);
return [sizeResult.width, sizeResult.height];
}
} catch (e) {
debugPrint('Failed to fetch dimensions in isolate: $e');
}

return [];
}

/// Retrieves the size of the given image given its bytes.
/// Uses the `image` package which does not support AVIF format. For AVIF images, use [processAvifImage] instead.
Future<Size> processImage(String filename) async {
Expand Down Expand Up @@ -159,13 +179,27 @@ Future<Size> retrieveImageDimensions({String? imageUrl, Uint8List? imageBytes})
Uint8List? data = imageBytes;

if (data == null && imageUrl != null) {
final file = await DefaultCacheManager().getSingleFile(imageUrl);
// Try to get size using partial content fetch
try {
final dimensions = await compute(processImageDimensions, imageUrl);

if (dimensions.isNotEmpty) {
size = Size(dimensions[0].toDouble(), dimensions[1].toDouble());
debugPrint('Retrieved image dimensions using partial content fetch: ${dimensions[0]}x${dimensions[1]}');
}
} catch (e) {
// Fallback to full download if partial fetch fails
debugPrint('Failed to retrieve image dimensions using partial content fetch: $e');
}

if (size == null) {
final file = await DefaultCacheManager().getSingleFile(imageUrl);

if (_isAvifImage(imageUrl) || _isAvifImage(file.path)) {
size = await processAvifImage(file.path);
} else {
// Other formats can be processed in a background isolate using the `image` package.
size = await compute(processImage, file.path);
if (_isAvifImage(imageUrl)) {
size = await processAvifImage(file.path);
} else {
size = await compute(processImage, file.path);
}
}
}

Expand Down
11 changes: 10 additions & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.7.1"
image_dimension_parser:
dependency: "direct main"
description:
path: "."
ref: d9bfec9dfe6a53b2d032129ce56311ec02796a29
resolved-ref: d9bfec9dfe6a53b2d032129ce56311ec02796a29
url: "https://github.com/thunder-app/image-dimension-parser.git"
source: git
version: "1.0.0"
image_picker:
dependency: "direct main"
description:
Expand Down Expand Up @@ -2053,5 +2062,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.10.0 <4.0.0"
dart: ">=3.10.4 <4.0.0"
flutter: ">=3.38.1"
4 changes: 4 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ dependencies:
git:
url: https://github.com/thunder-app/markdown-editor.git
ref: 34db147d5964b2e84f5a7e8edff4a5ce6b649ec8
image_dimension_parser:
git:
url: https://github.com/thunder-app/image-dimension-parser.git
ref: d9bfec9dfe6a53b2d032129ce56311ec02796a29
l10n_esperanto: ^2.0.14
android_intent_plus: ^6.0.0
app_links: ^7.0.0
Expand Down