From 4b19422598e72c3e89909cdce51e3dee3a4cd6f6 Mon Sep 17 00:00:00 2001 From: Komposten Date: Wed, 8 May 2019 19:05:46 +0200 Subject: [PATCH 1/4] :sparkles: Show "Read more" for long post (500+ chars) --- lib/widgets/post/post.dart | 23 +++-- .../post/widgets/post-body/post_body.dart | 21 +++-- .../post-body/widgets/post_body_text.dart | 93 +++++++++++++++---- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/lib/widgets/post/post.dart b/lib/widgets/post/post.dart index 11ac7c4e9..bb0ab5906 100644 --- a/lib/widgets/post/post.dart +++ b/lib/widgets/post/post.dart @@ -10,13 +10,20 @@ import 'package:Openbook/widgets/post/widgets/post_reactions/post_reactions.dart import 'package:Openbook/widgets/theming/post_divider.dart'; import 'package:flutter/material.dart'; -class OBPost extends StatelessWidget { +class OBPost extends StatefulWidget { final Post post; final OnPostDeleted onPostDeleted; const OBPost(this.post, {Key key, @required this.onPostDeleted}) : super(key: key); + @override + OBPostState createState() { + return OBPostState(); + } +} + +class OBPostState extends State { @override Widget build(BuildContext context) { return Column( @@ -25,17 +32,17 @@ class OBPost extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ OBPostHeader( - post: post, - onPostDeleted: onPostDeleted, + post: widget.post, + onPostDeleted: widget.onPostDeleted, ), - OBPostBody(post), - OBPostReactions(post), - OBPostCircles(post), + OBPostBody(widget.post), + OBPostReactions(widget.post), + OBPostCircles(widget.post), OBPostComments( - post, + widget.post, ), OBPostActions( - post, + widget.post, ), const SizedBox( height: 16, diff --git a/lib/widgets/post/widgets/post-body/post_body.dart b/lib/widgets/post/widgets/post-body/post_body.dart index d7e71ae91..a9df0c0c6 100644 --- a/lib/widgets/post/widgets/post-body/post_body.dart +++ b/lib/widgets/post/widgets/post-body/post_body.dart @@ -4,25 +4,32 @@ import 'package:Openbook/widgets/post/widgets/post-body/widgets/post_body_text.d import 'package:Openbook/widgets/post/widgets/post-body/widgets/post_body_video.dart'; import 'package:flutter/material.dart'; -class OBPostBody extends StatelessWidget { +class OBPostBody extends StatefulWidget { final Post post; const OBPostBody(this.post, {Key key}) : super(key: key); + @override + OBPostBodyState createState() { + return OBPostBodyState(); + } +} + +class OBPostBodyState extends State { @override Widget build(BuildContext context) { List bodyItems = []; - if (post.hasImage()) { + if (widget.post.hasImage()) { bodyItems.add(OBPostBodyImage( - post: post, + post: widget.post, )); - } else if (post.hasVideo()) { - bodyItems.add(OBPostBodyVideo(post: post)); + } else if (widget.post.hasVideo()) { + bodyItems.add(OBPostBodyVideo(post: widget.post)); } - if (post.hasText()) { - bodyItems.add(OBPostBodyText(post)); + if (widget.post.hasText()) { + bodyItems.add(OBPostBodyText(widget.post)); } return Row( diff --git a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart index d3ba89d04..b8ba9439c 100644 --- a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart +++ b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart @@ -5,12 +5,30 @@ import 'package:Openbook/widgets/theming/actionable_smart_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -class OBPostBodyText extends StatelessWidget { +class OBPostBodyText extends StatefulWidget { final Post _post; + + OBPostBodyText(this._post) : super(); + + @override + OBPostBodyTextState createState() { + return OBPostBodyTextState(); + } +} + +class OBPostBodyTextState extends State { + static const int _LENGTH_LIMIT = 500; + static const int _CUTOFF_OFFSET = 25; + ToastService _toastService; BuildContext _context; + bool _expanded; - OBPostBodyText(this._post); + @override + void initState() { + super.initState(); + _expanded = false; + } @override Widget build(BuildContext context) { @@ -19,35 +37,72 @@ class OBPostBodyText extends StatelessWidget { return GestureDetector( onLongPress: _copyText, - child: Padding( - padding: EdgeInsets.all(20.0), - child: _buildActionablePostText() - ) - ); + child: Padding(padding: EdgeInsets.all(20.0), child: _buildPostText())); } - Widget _buildActionablePostText() { + Widget _buildPostText() { return StreamBuilder( - stream: this._post.updateSubject, - initialData: this._post, + stream: widget._post.updateSubject, + initialData: widget._post, builder: (BuildContext context, AsyncSnapshot snapshot) { Post post = snapshot.data; + String postText = post.text; + bool isLongPost = postText.length > _LENGTH_LIMIT; - if (post.isEdited != null && post.isEdited) { - return OBActionableSmartText( - text: post.text, - trailingSmartTextElement: SecondaryTextElement(' (edited)'), - ); + if (isLongPost) { + return _buildCollapsiblePostText(postText, post, _expanded); } else { - return OBActionableSmartText( - text: post.text, - ); + return _buildActionablePostText(postText, post); } + }); + } + + Widget _buildCollapsiblePostText(String postText, Post post, bool expanded) { + if (!_expanded) { + var newLength = _LENGTH_LIMIT - _CUTOFF_OFFSET; + postText = postText.substring(0, newLength).trimRight() + '...'; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildActionablePostText(postText, post), + GestureDetector( + onTap: _toggleExpanded, + child: Padding( + padding: EdgeInsets.only(top: 5), + child: OBText( + expanded ? "Show less" : "Show more", + style: TextStyle(decoration: TextDecoration.underline), + textAlign: TextAlign.start, + ), + ), + ), + ], + ); + } + + Widget _buildActionablePostText(String postText, Post post) { + if (post.isEdited != null && post.isEdited) { + return OBActionableSmartText( + text: postText, + trailingSmartTextElement: SecondaryTextElement(' (edited)'), + ); + } else { + return OBActionableSmartText( + text: postText, + ); + } + } + + void _toggleExpanded() { + setState(() { + _expanded = !_expanded; }); } void _copyText() { - Clipboard.setData(ClipboardData(text: _post.text)); + Clipboard.setData(ClipboardData(text: widget._post.text)); _toastService.toast(message: 'Text copied!', context: _context, type: ToastType.info); } } From fc350e564bf563458b97f869ab8f30dfd88ea5f0 Mon Sep 17 00:00:00 2001 From: Komposten Date: Wed, 8 May 2019 23:11:17 +0200 Subject: [PATCH 2/4] :sparkles: Collapsing a post now scrolls up to the post. --- lib/pages/home/pages/community/community.dart | 73 +++++++-------- .../widgets/closed_posts.dart | 17 ++-- lib/pages/home/pages/post/post.dart | 21 +++-- lib/pages/home/pages/profile/profile.dart | 88 ++++++++++--------- .../pages/search/widgets/trending_posts.dart | 51 ++++++----- .../timeline/widgets/timeline-posts.dart | 17 ++-- .../post-body/widgets/post_body_text.dart | 46 +++++++++- lib/widgets/scroll_container.dart | 13 +++ 8 files changed, 199 insertions(+), 127 deletions(-) create mode 100644 lib/widgets/scroll_container.dart diff --git a/lib/pages/home/pages/community/community.dart b/lib/pages/home/pages/community/community.dart index df0f05edb..da090b796 100644 --- a/lib/pages/home/pages/community/community.dart +++ b/lib/pages/home/pages/community/community.dart @@ -18,6 +18,7 @@ import 'package:Openbook/widgets/buttons/community_floating_action_button.dart'; import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/post/post.dart'; import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:Openbook/widgets/theming/text.dart'; import 'package:async/async.dart'; @@ -230,41 +231,43 @@ class OBCommunityPageState extends State // the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can // find the NestedScrollView. builder: (BuildContext context) { - return CustomScrollView( - physics: const ClampingScrollPhysics(), - // The "controller" and "primary" members should be left - // unset, so that the NestedScrollView can control this - // inner scroll view. - // If the "controller" property is set, then this scroll - // view will not be associated with the NestedScrollView. - // The PageStorageKey should be unique to this ScrollView; - // it allows the list to remember its scroll position when - // the tab view is not on the screen. - key: PageStorageKey(0), - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor( - context), - ), - PagewiseSliverList( - noItemsFoundBuilder: (context) { - return OBCommunityNoPosts( - _community, - onWantsToRefreshCommunity: _refreshPosts, - ); - }, - loadingBuilder: (context) { - return const Padding( - padding: const EdgeInsets.all(20), - child: const OBProgressIndicator(), - ); - }, - pageLoadController: this._pageWiseController, - itemBuilder: _getPostItem, - ) - ], + return OBScrollContainer( + scroll: CustomScrollView( + physics: const ClampingScrollPhysics(), + // The "controller" and "primary" members should be left + // unset, so that the NestedScrollView can control this + // inner scroll view. + // If the "controller" property is set, then this scroll + // view will not be associated with the NestedScrollView. + // The PageStorageKey should be unique to this ScrollView; + // it allows the list to remember its scroll position when + // the tab view is not on the screen. + key: PageStorageKey(0), + slivers: [ + SliverOverlapInjector( + // This is the flip side of the SliverOverlapAbsorber above. + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + ), + PagewiseSliverList( + noItemsFoundBuilder: (context) { + return OBCommunityNoPosts( + _community, + onWantsToRefreshCommunity: _refreshPosts, + ); + }, + loadingBuilder: (context) { + return const Padding( + padding: const EdgeInsets.all(20), + child: const OBProgressIndicator(), + ); + }, + pageLoadController: this._pageWiseController, + itemBuilder: _getPostItem, + ) + ], + ), ); }, ), diff --git a/lib/pages/home/pages/community/pages/manage_community/pages/community_closed_posts/widgets/closed_posts.dart b/lib/pages/home/pages/community/pages/manage_community/pages/community_closed_posts/widgets/closed_posts.dart index 18dcd7b3a..cfa6e059e 100644 --- a/lib/pages/home/pages/community/pages/manage_community/pages/community_closed_posts/widgets/closed_posts.dart +++ b/lib/pages/home/pages/community/pages/manage_community/pages/community_closed_posts/widgets/closed_posts.dart @@ -8,6 +8,7 @@ import 'package:Openbook/services/httpie.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/services/user.dart'; import 'package:Openbook/widgets/post/post.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/secondary_text.dart'; import 'package:Openbook/widgets/theming/text.dart'; import 'package:Openbook/widgets/tiles/loading_indicator_tile.dart'; @@ -79,13 +80,15 @@ class OBCommunityClosedPostsState extends State { } Widget _buildClosedPosts() { - return ListView.builder( - controller: _postsScrollController, - physics: const ClampingScrollPhysics(), - cacheExtent: 30, - padding: const EdgeInsets.all(0), - itemCount: _posts.length, - itemBuilder: _buildPostItem); + return OBScrollContainer( + scroll: ListView.builder( + controller: _postsScrollController, + physics: const ClampingScrollPhysics(), + cacheExtent: 30, + padding: const EdgeInsets.all(0), + itemCount: _posts.length, + itemBuilder: _buildPostItem), + ); } Widget _buildPostItem(BuildContext context, int index) { diff --git a/lib/pages/home/pages/post/post.dart b/lib/pages/home/pages/post/post.dart index 15b4ab7a9..a7801b11a 100644 --- a/lib/pages/home/pages/post/post.dart +++ b/lib/pages/home/pages/post/post.dart @@ -5,6 +5,7 @@ import 'package:Openbook/provider.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/services/user.dart'; import 'package:Openbook/widgets/post/post.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -57,15 +58,17 @@ class OBPostPageState extends State { Expanded( child: RefreshIndicator( key: _refreshIndicatorKey, - child: ListView( - padding: const EdgeInsets.all(0), - physics: const AlwaysScrollableScrollPhysics(), - children: [ - StreamBuilder( - stream: widget.post.updateSubject, - initialData: widget.post, - builder: _buildPost) - ], + child: OBScrollContainer( + scroll: ListView( + padding: const EdgeInsets.all(0), + physics: const AlwaysScrollableScrollPhysics(), + children: [ + StreamBuilder( + stream: widget.post.updateSubject, + initialData: widget.post, + builder: _buildPost) + ], + ), ), onRefresh: _refreshPost), ), diff --git a/lib/pages/home/pages/profile/profile.dart b/lib/pages/home/pages/profile/profile.dart index 4e0b853d8..64a9a3a09 100644 --- a/lib/pages/home/pages/profile/profile.dart +++ b/lib/pages/home/pages/profile/profile.dart @@ -11,6 +11,7 @@ import 'package:Openbook/services/toast.dart'; import 'package:Openbook/services/user.dart'; import 'package:Openbook/widgets/post/post.dart'; import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:async/async.dart'; import 'package:flutter/cupertino.dart'; @@ -94,54 +95,57 @@ class OBProfilePageState extends State { whenEmptyLoad: false, isFinish: !_morePostsToLoad, delegate: OBHomePostsLoadMoreDelegate(), - child: ListView.builder( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - padding: EdgeInsets.all(0), - itemCount: _posts.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - Widget postsItem; - - if (_refreshPostsInProgress && _posts.isEmpty) { - postsItem = SizedBox( - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 20), - child: OBProgressIndicator(), + child: OBScrollContainer( + scroll: ListView.builder( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.all(0), + itemCount: _posts.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + Widget postsItem; + + if (_refreshPostsInProgress && + _posts.isEmpty) { + postsItem = SizedBox( + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 20), + child: OBProgressIndicator(), + ), ), - ), - ); - } else if (_posts.length == 0) { - postsItem = OBProfileNoPosts( - _user, - onWantsToRefreshProfile: _refresh, - ); - } else { - postsItem = const SizedBox( - height: 20, + ); + } else if (_posts.length == 0) { + postsItem = OBProfileNoPosts( + _user, + onWantsToRefreshProfile: _refresh, + ); + } else { + postsItem = const SizedBox( + height: 20, + ); + } + + return Column( + children: [ + OBProfileCover(_user), + OBProfileCard( + _user, + ), + postsItem + ], ); } - return Column( - children: [ - OBProfileCover(_user), - OBProfileCard( - _user, - ), - postsItem - ], - ); - } - - int postIndex = index - 1; + int postIndex = index - 1; - var post = _posts[postIndex]; + var post = _posts[postIndex]; - return OBPost(post, - onPostDeleted: _onPostDeleted, - key: Key(post.id.toString())); - }), + return OBPost(post, + onPostDeleted: _onPostDeleted, + key: Key(post.id.toString())); + }), + ), onLoadMore: _loadMorePosts), onRefresh: _refresh), ) diff --git a/lib/pages/home/pages/search/widgets/trending_posts.dart b/lib/pages/home/pages/search/widgets/trending_posts.dart index 84648f4b4..e6523265c 100644 --- a/lib/pages/home/pages/search/widgets/trending_posts.dart +++ b/lib/pages/home/pages/search/widgets/trending_posts.dart @@ -8,6 +8,7 @@ import 'package:Openbook/services/user.dart'; import 'package:Openbook/widgets/alerts/button_alert.dart'; import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/post/post.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/primary_accent_text.dart'; import 'package:flutter/material.dart'; @@ -66,30 +67,32 @@ class OBTrendingPostsState extends State { return _posts.isEmpty && _getTrendingPostsSubscription == null ? _buildNoTrendingPostsAlert() : RefreshIndicator( - key: _refreshIndicatorKey, - child: ListView.builder( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.all(0), - itemCount: _posts.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 10), - child: OBPrimaryAccentText('Trending posts', - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 24)), - ); - } - - return OBPost( - _posts[index - 1], - onPostDeleted: _onPostDeleted, - ); - }), - onRefresh: refresh, - ); + key: _refreshIndicatorKey, + child: OBScrollContainer( + scroll: ListView.builder( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.all(0), + itemCount: _posts.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + child: OBPrimaryAccentText('Trending posts', + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 24)), + ); + } + + return OBPost( + _posts[index - 1], + onPostDeleted: _onPostDeleted, + ); + }), + ), + onRefresh: refresh, + ); } Widget _buildNoTrendingPostsAlert() { diff --git a/lib/pages/home/pages/timeline/widgets/timeline-posts.dart b/lib/pages/home/pages/timeline/widgets/timeline-posts.dart index ab041ff3d..2c1a6ab4d 100644 --- a/lib/pages/home/pages/timeline/widgets/timeline-posts.dart +++ b/lib/pages/home/pages/timeline/widgets/timeline-posts.dart @@ -12,6 +12,7 @@ import 'package:Openbook/services/user.dart'; import 'package:Openbook/widgets/buttons/button.dart'; import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/post/post.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/secondary_text.dart'; import 'package:Openbook/widgets/theming/text.dart'; import 'package:Openbook/widgets/tiles/loading_indicator_tile.dart'; @@ -94,13 +95,15 @@ class OBTimelinePostsState extends State { } Widget _buildTimelinePosts() { - return ListView.builder( - controller: _postsScrollController, - physics: const ClampingScrollPhysics(), - cacheExtent: 30, - padding: const EdgeInsets.all(0), - itemCount: _posts.length, - itemBuilder: _buildTimelinePost); + return OBScrollContainer( + scroll: ListView.builder( + controller: _postsScrollController, + physics: const ClampingScrollPhysics(), + cacheExtent: 30, + padding: const EdgeInsets.all(0), + itemCount: _posts.length, + itemBuilder: _buildTimelinePost), + ); } Widget _buildTimelinePost(BuildContext context, int index) { diff --git a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart index b8ba9439c..0df6ecd3f 100644 --- a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart +++ b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart @@ -1,6 +1,7 @@ import 'package:Openbook/provider.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/models/post.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/actionable_smart_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -22,11 +23,19 @@ class OBPostBodyTextState extends State { ToastService _toastService; BuildContext _context; + ScrollView _scroll; bool _expanded; + double _heightCollapsed; + double _heightExpanded; + double _minScrollOffset; + double _maxScrollOffset; + bool _doScroll = false; + @override void initState() { super.initState(); + //Must default to false, otherwise the scrolling on collapse won't work properly for the first collapse. _expanded = false; } @@ -34,10 +43,17 @@ class OBPostBodyTextState extends State { Widget build(BuildContext context) { _toastService = OpenbookProvider.of(context).toastService; _context = context; + var scrollContainer = (_context.inheritFromWidgetOfExactType(OBScrollContainer) as OBScrollContainer); + if (scrollContainer != null) + _scroll = scrollContainer.scroll; + + //This is async, so it will be executed after build completes. + _afterLayout(); return GestureDetector( - onLongPress: _copyText, - child: Padding(padding: EdgeInsets.all(20.0), child: _buildPostText())); + onLongPress: _copyText, + child: Padding(padding: EdgeInsets.all(20.0), child: _buildPostText()), + ); } Widget _buildPostText() { @@ -96,13 +112,37 @@ class OBPostBodyTextState extends State { } void _toggleExpanded() { + if (!_expanded) { + _heightCollapsed = context.size.height; + if (_hasScrollController()) { + _minScrollOffset = _scroll.controller.position.minScrollExtent + 0.1; + _maxScrollOffset = _scroll.controller.position.maxScrollExtent - 0.1; + } + } else { + _heightExpanded = context.size.height; + } + setState(() { + _doScroll = _expanded; _expanded = !_expanded; }); } + bool _hasScrollController() { + return _scroll != null && _scroll.controller != null; + } + + void _afterLayout() async { + if (_doScroll && _hasScrollController()) { + var scrollOffset = _scroll.controller.offset - (_heightExpanded - _heightCollapsed); + _scroll.controller.jumpTo(scrollOffset.clamp(_minScrollOffset, _maxScrollOffset)); + _doScroll = false; + } + } + void _copyText() { Clipboard.setData(ClipboardData(text: widget._post.text)); - _toastService.toast(message: 'Text copied!', context: _context, type: ToastType.info); + _toastService.toast( + message: 'Text copied!', context: _context, type: ToastType.info); } } diff --git a/lib/widgets/scroll_container.dart b/lib/widgets/scroll_container.dart new file mode 100644 index 000000000..2c7ab487d --- /dev/null +++ b/lib/widgets/scroll_container.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class OBScrollContainer extends InheritedWidget { + final ScrollView scroll; + + const OBScrollContainer({Key key, this.scroll}) + : super(key: key, child: scroll); + + @override + bool updateShouldNotify(OBScrollContainer old) { + return false; + } +} From 1b0d3182d0f1415379b7d8706aa0deac3454e344 Mon Sep 17 00:00:00 2001 From: Komposten Date: Thu, 9 May 2019 09:07:57 +0200 Subject: [PATCH 3/4] :bug: Fix (non-breaking) exception when collapsing posts controller.jumpTo was called during the build phase and triggered methods that aren't allowed to be called there. --- .../post-body/widgets/post_body_text.dart | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart index 0df6ecd3f..20855c1c0 100644 --- a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart +++ b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart @@ -47,9 +47,6 @@ class OBPostBodyTextState extends State { if (scrollContainer != null) _scroll = scrollContainer.scroll; - //This is async, so it will be executed after build completes. - _afterLayout(); - return GestureDetector( onLongPress: _copyText, child: Padding(padding: EdgeInsets.all(20.0), child: _buildPostText()), @@ -115,8 +112,8 @@ class OBPostBodyTextState extends State { if (!_expanded) { _heightCollapsed = context.size.height; if (_hasScrollController()) { - _minScrollOffset = _scroll.controller.position.minScrollExtent + 0.1; - _maxScrollOffset = _scroll.controller.position.maxScrollExtent - 0.1; + _minScrollOffset = _scroll.controller.position.minScrollExtent; + _maxScrollOffset = _scroll.controller.position.maxScrollExtent; } } else { _heightExpanded = context.size.height; @@ -125,6 +122,12 @@ class OBPostBodyTextState extends State { setState(() { _doScroll = _expanded; _expanded = !_expanded; + + if (_doScroll && _hasScrollController()) { + var scrollOffset = _scroll.controller.offset - (_heightExpanded - _heightCollapsed); + _scroll.controller.jumpTo(scrollOffset.clamp(_minScrollOffset, _maxScrollOffset)); + _doScroll = false; + } }); } @@ -132,14 +135,6 @@ class OBPostBodyTextState extends State { return _scroll != null && _scroll.controller != null; } - void _afterLayout() async { - if (_doScroll && _hasScrollController()) { - var scrollOffset = _scroll.controller.offset - (_heightExpanded - _heightCollapsed); - _scroll.controller.jumpTo(scrollOffset.clamp(_minScrollOffset, _maxScrollOffset)); - _doScroll = false; - } - } - void _copyText() { Clipboard.setData(ClipboardData(text: widget._post.text)); _toastService.toast( From 263d278072e32ee2f606a1cbfbfea42775e20e1a Mon Sep 17 00:00:00 2001 From: Komposten Date: Sat, 11 May 2019 22:20:08 +0200 Subject: [PATCH 4/4] :bug: Fixed links cut off by post collapse not working If a link, mention or community was cut off by the post collapse function, the link would break (e.g. if http://openbook.social was cut down to http://openbook.so... it would still be a link but not to the correct place). --- .../post-body/widgets/post_body_text.dart | 11 ++- .../theming/actionable_smart_text.dart | 6 ++ lib/widgets/theming/smart_text.dart | 69 +++++++++++++++---- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart index 20855c1c0..5f207215e 100644 --- a/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart +++ b/lib/widgets/post/widgets/post-body/widgets/post_body_text.dart @@ -71,15 +71,12 @@ class OBPostBodyTextState extends State { } Widget _buildCollapsiblePostText(String postText, Post post, bool expanded) { - if (!_expanded) { - var newLength = _LENGTH_LIMIT - _CUTOFF_OFFSET; - postText = postText.substring(0, newLength).trimRight() + '...'; - } + var maxlength = expanded ? null : _LENGTH_LIMIT - _CUTOFF_OFFSET; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildActionablePostText(postText, post), + _buildActionablePostText(postText, post, maxlength: maxlength), GestureDetector( onTap: _toggleExpanded, child: Padding( @@ -95,15 +92,17 @@ class OBPostBodyTextState extends State { ); } - Widget _buildActionablePostText(String postText, Post post) { + Widget _buildActionablePostText(String postText, Post post, {int maxlength}) { if (post.isEdited != null && post.isEdited) { return OBActionableSmartText( text: postText, trailingSmartTextElement: SecondaryTextElement(' (edited)'), + maxlength: maxlength, ); } else { return OBActionableSmartText( text: postText, + maxlength: maxlength, ); } } diff --git a/lib/widgets/theming/actionable_smart_text.dart b/lib/widgets/theming/actionable_smart_text.dart index 26b4d81a5..1e0ec2776 100644 --- a/lib/widgets/theming/actionable_smart_text.dart +++ b/lib/widgets/theming/actionable_smart_text.dart @@ -13,15 +13,19 @@ import 'package:flutter/material.dart'; class OBActionableSmartText extends StatefulWidget { final String text; + final int maxlength; final OBTextSize size; final TextOverflow overflow; + final TextOverflow lengthOverflow; final SmartTextElement trailingSmartTextElement; const OBActionableSmartText({ Key key, this.text, + this.maxlength, this.size = OBTextSize.medium, this.overflow = TextOverflow.clip, + this.lengthOverflow = TextOverflow.ellipsis, this.trailingSmartTextElement }) : super(key: key); @@ -64,7 +68,9 @@ class OBActionableTextState extends State { return OBSmartText( text: widget.text, + maxlength: widget.maxlength, overflow: widget.overflow, + lengthOverflow: widget.lengthOverflow, onCommunityNameTapped: _onCommunityNameTapped, onUsernameTapped: _onUsernameTapped, onLinkTapped: _onLinkTapped, diff --git a/lib/widgets/theming/smart_text.dart b/lib/widgets/theming/smart_text.dart index 2ad7f16f6..b3f8298dc 100644 --- a/lib/widgets/theming/smart_text.dart +++ b/lib/widgets/theming/smart_text.dart @@ -11,13 +11,18 @@ import 'package:tinycolor/tinycolor.dart'; // Based on https://github.com/knoxpo/flutter_smart_text_view -abstract class SmartTextElement {} +abstract class SmartTextElement { + /// Stores the text used when rendering the element + String text; + + SmartTextElement(this.text); +} /// Represents an element containing a link class LinkElement extends SmartTextElement { final String url; - LinkElement(this.url); + LinkElement(this.url) : super(url); @override String toString() { @@ -29,7 +34,7 @@ class LinkElement extends SmartTextElement { class HashTagElement extends SmartTextElement { final String tag; - HashTagElement(this.tag); + HashTagElement(this.tag) : super(tag); @override String toString() { @@ -41,7 +46,7 @@ class HashTagElement extends SmartTextElement { class UsernameElement extends SmartTextElement { final String username; - UsernameElement(this.username); + UsernameElement(this.username) : super(username); @override String toString() { @@ -53,7 +58,7 @@ class UsernameElement extends SmartTextElement { class CommunityNameElement extends SmartTextElement { final String communityName; - CommunityNameElement(this.communityName); + CommunityNameElement(this.communityName) : super(communityName); @override String toString() { @@ -63,9 +68,7 @@ class CommunityNameElement extends SmartTextElement { /// Represents an element containing text class TextElement extends SmartTextElement { - final String text; - - TextElement(this.text); + TextElement(String text) : super(text); @override String toString() { @@ -75,9 +78,7 @@ class TextElement extends SmartTextElement { /// Represents an element containing secondary text class SecondaryTextElement extends SmartTextElement { - final String text; - - SecondaryTextElement(this.text); + SecondaryTextElement(String text) : super(text); @override String toString() { @@ -179,6 +180,9 @@ class OBSmartText extends StatelessWidget { /// Text to be linkified final String text; + /// Maximum text length + final int maxlength; + /// Style for non-link text final TextStyle style; @@ -207,10 +211,14 @@ class OBSmartText extends StatelessWidget { final TextOverflow overflow; + final TextOverflow lengthOverflow; + const OBSmartText({ Key key, this.text, + this.maxlength, this.overflow = TextOverflow.clip, + this.lengthOverflow = TextOverflow.ellipsis, this.style, this.linkStyle, this.tagStyle, @@ -270,6 +278,10 @@ class OBSmartText extends StatelessWidget { List elements = _smartify(text); + if (this.maxlength != null && text.length > maxlength) { + _enforceMaxLength(elements, maxlength); + } + if (this.trailingSmartTextElement != null) { elements.add(this.trailingSmartTextElement); } @@ -288,25 +300,25 @@ class OBSmartText extends StatelessWidget { ); } else if (element is LinkElement) { return LinkTextSpan( - text: element.url, + text: element.text, style: linkStyle, onPressed: () => _onOpen(element.url), ); } else if (element is HashTagElement) { return LinkTextSpan( - text: element.tag, + text: element.text, style: tagStyle, onPressed: () => _onTagTapped(element.tag), ); } else if (element is UsernameElement) { return LinkTextSpan( - text: element.username, + text: element.text, style: usernameStyle, onPressed: () => _onUsernameTapped(element.username), ); } else if (element is CommunityNameElement) { return LinkTextSpan( - text: element.communityName, + text: element.text, style: communityNameStyle, onPressed: () => _onCommunityNameTapped(element.communityName), ); @@ -314,6 +326,33 @@ class OBSmartText extends StatelessWidget { }).toList()); } + void _enforceMaxLength(List elements, int maxlength) { + int length = 0; + + if (lengthOverflow == TextOverflow.visible) { + return; + } else if (lengthOverflow == TextOverflow.ellipsis) { + maxlength -= 3; + } + + for (int i = 0; i < elements.length; i++) { + var element = elements[i]; + var elementLength = element.text.length; + + if (length + elementLength > maxlength) { + elements.removeRange(i+1, elements.length); + element.text = element.text.substring(0, maxlength - length).trimRight(); + + if (lengthOverflow == TextOverflow.ellipsis) { + element.text = element.text + '...'; + } + break; + } else { + length += elementLength; + } + } + } + @override Widget build(BuildContext context) { OpenbookProviderState openbookProvider = OpenbookProvider.of(context);