Compare commits

...

2 Commits

Author SHA1 Message Date
9312c56dd0 v0.2.17 (#47)
* improved story screen scrolling.

* bumped version.

* shrink instead of return on tapping back button. #42

* allowed users to view other user's profile. #45

* bumped version.

* added back underline to links.

* fixed overlow of popup menu,
2022-06-10 02:10:23 -07:00
6e71de5913 updated README.md 2022-06-05 22:39:54 -07:00
19 changed files with 512 additions and 269 deletions

View File

@ -4,7 +4,6 @@
A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough. A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Play Store](https://img.shields.io/badge/Play%20Store--yellow)](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
[![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
[![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest) [![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest)
[![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev) [![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev)

View File

@ -0,0 +1,6 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -568,7 +568,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -577,7 +577,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.16; MARKETING_VERSION = 0.2.17;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -705,7 +705,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -714,7 +714,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.16; MARKETING_VERSION = 0.2.17;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -736,7 +736,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -745,7 +745,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.16; MARKETING_VERSION = 0.2.17;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
@ -159,10 +160,19 @@ class CommentsCubit extends Cubit<CommentsState> {
..addKid(comment.id, to: comment.parent) ..addKid(comment.id, to: comment.parent)
..cacheComment(comment); ..cacheComment(comment);
_sembastRepository.cacheComment(comment); _sembastRepository.cacheComment(comment);
final List<LinkifyElement> elements = linkify(
comment.text,
);
final BuildableComment buildableComment =
BuildableComment.fromComment(comment, elements: elements);
final List<Comment> updatedComments = <Comment>[ final List<Comment> updatedComments = <Comment>[
...state.comments, ...state.comments,
comment buildableComment
]; ];
emit(state.copyWith(comments: updatedComments)); emit(state.copyWith(comments: updatedComments));
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage && if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
@ -180,6 +190,31 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
} }
List<LinkifyElement> linkify(
String text, {
LinkifyOptions options = const LinkifyOptions(),
List<Linkifier> linkifiers = const <Linkifier>[
UrlLinkifier(),
EmailLinkifier(),
],
}) {
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
if (text.isEmpty) {
return <LinkifyElement>[];
}
if (linkifiers.isEmpty) {
return list;
}
for (final Linkifier linkifier in linkifiers) {
list = linkifier.parse(list, options);
}
return list;
}
@override @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();

View File

@ -14,4 +14,5 @@ export 'search/search_cubit.dart';
export 'split_view/split_view_cubit.dart'; export 'split_view/split_view_cubit.dart';
export 'submit/submit_cubit.dart'; export 'submit/submit_cubit.dart';
export 'time_machine/time_machine_cubit.dart'; export 'time_machine/time_machine_cubit.dart';
export 'user/user_cubit.dart';
export 'vote/vote_cubit.dart'; export 'vote/vote_cubit.dart';

View File

@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
part 'user_state.dart';
class UserCubit extends Cubit<UserState> {
UserCubit({StoriesRepository? storiesRepository})
: _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
super(UserState.init());
final StoriesRepository _storiesRepository;
void init({required String userId}) {
emit(state.copyWith(status: UserStatus.loading));
_storiesRepository.fetchUserBy(userId: userId).then((User user) {
emit(state.copyWith(user: user, status: UserStatus.loaded));
}).onError((_, __) {
emit(state.copyWith(status: UserStatus.failure));
return;
});
}
}

View File

@ -0,0 +1,38 @@
part of 'user_cubit.dart';
enum UserStatus {
initial,
loading,
loaded,
failure,
}
class UserState extends Equatable {
const UserState({
required this.user,
required this.status,
});
UserState.init()
: user = User.empty(),
status = UserStatus.initial;
final User user;
final UserStatus status;
UserState copyWith({
User? user,
UserStatus? status,
}) {
return UserState(
user: user ?? this.user,
status: status ?? this.status,
);
}
@override
List<Object?> get props => <Object?>[
user,
status,
];
}

View File

@ -0,0 +1,33 @@
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/models/comment.dart';
import 'package:hacki/models/models.dart';
class BuildableComment extends Comment {
BuildableComment({
required super.id,
required super.time,
required super.parent,
required super.score,
required super.by,
required super.text,
required super.kids,
required super.deleted,
required super.level,
required this.elements,
});
BuildableComment.fromComment(Comment comment, {required this.elements})
: super(
id: comment.id,
time: comment.time,
parent: comment.parent,
score: comment.score,
by: comment.by,
text: comment.text,
kids: comment.kids,
deleted: comment.deleted,
level: comment.level,
);
final List<LinkifyElement> elements;
}

View File

@ -1,3 +1,4 @@
export 'buildable_comment.dart';
export 'comment.dart'; export 'comment.dart';
export 'item.dart'; export 'item.dart';
export 'poll_option.dart'; export 'poll_option.dart';

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:intl/intl.dart';
class User { class User {
User({ User({
required this.about, required this.about,
@ -29,6 +31,12 @@ class User {
final String id; final String id;
final int karma; final int karma;
static final DateFormat _dateTimeFormatter = DateFormat.yMMMd();
String get description {
return '''$karma karma, created on ${_dateTimeFormatter.format(DateTime.fromMillisecondsSinceEpoch(created * 1000))}''';
}
@override @override
String toString() { String toString() {
final String prettyString = final String prettyString =

View File

@ -171,7 +171,7 @@ class _HomeScreenState extends State<HomeScreen>
child: Container( child: Container(
color: Colors.orangeAccent.withOpacity(0.2), color: Colors.orangeAccent.withOpacity(0.2),
child: StoryTile( child: StoryTile(
key: ObjectKey(story), key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story, story: story,
onTap: () => onStoryTapped(story, isPin: true), onTap: () => onStoryTapped(story, isPin: true),
showWebPreview: preferenceState.showComplexStoryTile, showWebPreview: preferenceState.showComplexStoryTile,
@ -543,7 +543,7 @@ class _TabletStoryView extends StatelessWidget {
previous.storyScreenArgs != current.storyScreenArgs, previous.storyScreenArgs != current.storyScreenArgs,
builder: (BuildContext context, SplitViewState state) { builder: (BuildContext context, SplitViewState state) {
if (state.storyScreenArgs != null) { if (state.storyScreenArgs != null) {
return StoryScreen.build(state.storyScreenArgs!); return StoryScreen.build(context, state.storyScreenArgs!);
} }
return Material( return Material(

View File

@ -407,7 +407,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.2.16', applicationVersion: 'v0.2.17',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(12), Radius.circular(12),

View File

@ -88,8 +88,17 @@ class StoryScreen extends StatefulWidget {
); );
} }
static Widget build(StoryScreenArgs args) { static Widget build(BuildContext context, StoryScreenArgs args) {
return MultiBlocProvider( return WillPopScope(
onWillPop: () async {
if (context.read<SplitViewCubit>().state.expanded) {
context.read<SplitViewCubit>().zoom();
return false;
} else {
return true;
}
},
child: MultiBlocProvider(
key: ValueKey<StoryScreenArgs>(args), key: ValueKey<StoryScreenArgs>(args),
providers: <BlocProvider<dynamic>>[ providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>( BlocProvider<CommentsCubit>(
@ -112,6 +121,7 @@ class StoryScreen extends StatefulWidget {
parentComments: args.targetComments ?? <Comment>[], parentComments: args.targetComments ?? <Comment>[],
splitViewEnabled: true, splitViewEnabled: true,
), ),
),
); );
} }
@ -140,7 +150,6 @@ class _StoryScreenState extends State<StoryScreen> {
delay: _featureDiscoveryDismissThrottleDelay, delay: _featureDiscoveryDismissThrottleDelay,
); );
static const int _extraItemsCount = 2;
static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2); static const Duration _storyLinkTapThrottleDelay = Duration(seconds: 2);
static const Duration _featureDiscoveryDismissThrottleDelay = static const Duration _featureDiscoveryDismissThrottleDelay =
Duration(seconds: 1); Duration(seconds: 1);
@ -270,12 +279,8 @@ class _StoryScreenState extends State<StoryScreen> {
onLoading: () { onLoading: () {
context.read<CommentsCubit>().loadMore(); context.read<CommentsCubit>().loadMore();
}, },
child: ListView.builder( child: ListView(
primary: false, primary: false,
itemCount: state.comments.length + _extraItemsCount,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Column(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
height: topPadding, height: topPadding,
@ -294,10 +299,7 @@ class _StoryScreenState extends State<StoryScreen> {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
if (widget.story != if (widget.story !=
context context.read<EditCubit>().state.replyingTo) {
.read<EditCubit>()
.state
.replyingTo) {
commentEditingController.clear(); commentEditingController.clear();
} }
context context
@ -377,13 +379,13 @@ class _StoryScreenState extends State<StoryScreen> {
child: SelectableLinkify( child: SelectableLinkify(
text: widget.story.text, text: widget.story.text,
style: TextStyle( style: TextStyle(
fontSize: MediaQuery.of(context) fontSize:
.textScaleFactor * MediaQuery.of(context).textScaleFactor *
15, 15,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: MediaQuery.of(context) fontSize:
.textScaleFactor * MediaQuery.of(context).textScaleFactor *
15, 15,
color: Colors.orange, color: Colors.orange,
), ),
@ -414,9 +416,8 @@ class _StoryScreenState extends State<StoryScreen> {
), ),
if (state.onlyShowTargetComment) ...<Widget>[ if (state.onlyShowTargetComment) ...<Widget>[
TextButton( TextButton(
onPressed: () => context onPressed: () =>
.read<CommentsCubit>() context.read<CommentsCubit>().loadAll(widget.story),
.loadAll(widget.story),
child: const Text('View all comments'), child: const Text('View all comments'),
), ),
const Divider( const Divider(
@ -424,8 +425,7 @@ class _StoryScreenState extends State<StoryScreen> {
), ),
], ],
if (state.comments.isEmpty && if (state.comments.isEmpty &&
state.status == state.status == CommentsStatus.allLoaded) ...<Widget>[
CommentsStatus.allLoaded) ...<Widget>[
const SizedBox( const SizedBox(
height: 240, height: 240,
), ),
@ -436,28 +436,9 @@ class _StoryScreenState extends State<StoryScreen> {
), ),
), ),
], ],
], for (final Comment comment in state.comments)
); FadeIn(
} else if (index == key: ValueKey<String>('${comment.id}-FadeIn'),
state.comments.length + _extraItemsCount - 1) {
if ((state.status == CommentsStatus.allLoaded &&
state.comments.isNotEmpty) ||
state.onlyShowTargetComment) {
return SizedBox(
height: 240,
child: Center(
child: Text(happyFace),
),
);
}
return const SizedBox.shrink();
}
final Comment comment = state.comments.elementAt(index - 1);
return FadeIn(
key: ValueKey<int>(comment.id),
child: CommentTile( child: CommentTile(
comment: comment, comment: comment,
level: comment.level, level: comment.level,
@ -491,8 +472,17 @@ class _StoryScreenState extends State<StoryScreen> {
onStoryLinkTapped: onStoryLinkTapped, onStoryLinkTapped: onStoryLinkTapped,
onTimeMachineActivated: onTimeMachineActivated, onTimeMachineActivated: onTimeMachineActivated,
), ),
); ),
}, if ((state.status == CommentsStatus.allLoaded &&
state.comments.isNotEmpty) ||
state.onlyShowTargetComment)
SizedBox(
height: 240,
child: Center(
child: Text(happyFace),
),
)
],
), ),
); );
@ -766,12 +756,82 @@ class _StoryScreenState extends State<StoryScreen> {
final bool upvoted = voteState.vote == Vote.up; final bool upvoted = voteState.vote == Vote.up;
final bool downvoted = voteState.vote == Vote.down; final bool downvoted = voteState.vote == Vote.down;
return Container( return Container(
height: 300, height: item is Comment ? 370 : 390,
color: Theme.of(context).canvasColor, color: Theme.of(context).canvasColor,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
BlocProvider<UserCubit>(
create: (BuildContext context) =>
UserCubit()..init(userId: item.by),
child: BlocBuilder<UserCubit, UserState>(
builder: (BuildContext context, UserState state) {
return ListTile(
leading: const Icon(
Icons.account_circle,
),
title: Text(item.by),
subtitle: Text(
state.user.description,
),
onTap: () {
showDialog<void>(
context: context,
builder: (BuildContext context) =>
SimpleDialog(
title: Text('About ${state.user.id}'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
children: <Widget>[
SelectableLinkify(
text: HtmlUtil.parseHtml(
state.user.about,
),
linkStyle: const TextStyle(
color: Colors.orange,
),
onOpen: (LinkableElement link) {
if (link.url.contains(
'news.ycombinator.com/item',
)) {
onStoryLinkTapped.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
ButtonBar(
children: <Widget>[
ElevatedButton(
onPressed: () =>
Navigator.pop(context),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(
Colors.deepOrange,
),
),
child: const Text(
'Okay',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
],
),
);
},
);
},
),
),
ListTile( ListTile(
leading: Icon( leading: Icon(
FeatherIcons.chevronUp, FeatherIcons.chevronUp,

View File

@ -25,6 +25,7 @@ class FavIconButton extends StatelessWidget {
builder: (BuildContext context, FavState favState) { builder: (BuildContext context, FavState favState) {
final bool isFav = favState.favIds.contains(storyId); final bool isFav = favState.favIds.contains(storyId);
return IconButton( return IconButton(
tooltip: 'Add to favorites',
icon: DescribedFeatureOverlay( icon: DescribedFeatureOverlay(
onBackgroundTap: onBackgroundTap, onBackgroundTap: onBackgroundTap,
onDismiss: onDismiss, onDismiss: onDismiss,

View File

@ -21,6 +21,7 @@ class LinkIconButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
tooltip: 'Open this story in browser',
icon: DescribedFeatureOverlay( icon: DescribedFeatureOverlay(
onBackgroundTap: onBackgroundTap, onBackgroundTap: onBackgroundTap,
onDismiss: onDismiss, onDismiss: onDismiss,

View File

@ -31,6 +31,7 @@ class PinIconButton extends StatelessWidget {
child: Transform.translate( child: Transform.translate(
offset: const Offset(2, 0), offset: const Offset(2, 0),
child: IconButton( child: IconButton(
tooltip: 'Pin to home screen',
icon: DescribedFeatureOverlay( icon: DescribedFeatureOverlay(
onBackgroundTap: onBackgroundTap, onBackgroundTap: onBackgroundTap,
onDismiss: onDismiss, onDismiss: onDismiss,

View File

@ -38,6 +38,7 @@ class CommentTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<CollapseCubit>( return BlocProvider<CollapseCubit>(
key: ValueKey<String>('${comment.id}-BlocProvider'),
lazy: false, lazy: false,
create: (_) => CollapseCubit( create: (_) => CollapseCubit(
commentId: comment.id, commentId: comment.id,
@ -209,8 +210,39 @@ class CommentTile extends StatelessWidget {
top: 6, top: 6,
bottom: 12, bottom: 12,
), ),
child: SelectableLinkify( child: comment is BuildableComment
key: ObjectKey(comment), ? SelectableText.rich(
key: ValueKey<int>(comment.id),
buildTextSpan(
(comment as BuildableComment)
.elements,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
15,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
15,
decoration:
TextDecoration.underline,
color: Colors.orange,
),
onOpen: (LinkableElement link) {
if (link.url.contains(
'news.ycombinator.com/item',
)) {
onStoryLinkTapped
.call(link.url);
} else {
LinkUtil.launch(link.url);
}
},
),
)
: SelectableLinkify(
key: ValueKey<int>(comment.id),
text: comment.text, text: comment.text,
style: TextStyle( style: TextStyle(
fontSize: MediaQuery.of(context) fontSize: MediaQuery.of(context)
@ -227,7 +259,8 @@ class CommentTile extends StatelessWidget {
if (link.url.contains( if (link.url.contains(
'news.ycombinator.com/item', 'news.ycombinator.com/item',
)) { )) {
onStoryLinkTapped.call(link.url); onStoryLinkTapped
.call(link.url);
} else { } else {
LinkUtil.launch(link.url); LinkUtil.launch(link.url);
} }

View File

@ -87,7 +87,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
) )
: null, : null,
child: StoryTile( child: StoryTile(
key: ObjectKey(e), key: ValueKey<int>(e.id),
story: e, story: e,
onTap: () => onTap(e), onTap: () => onTap(e),
showWebPreview: showWebPreview, showWebPreview: showWebPreview,

View File

@ -1,6 +1,6 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 0.2.16+55 version: 0.2.17+56
publish_to: none publish_to: none
environment: environment: