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/services/validation.dart b/lib/services/validation.dart index da52ef787..07066f246 100644 --- a/lib/services/validation.dart +++ b/lib/services/validation.dart @@ -19,8 +19,8 @@ class ValidationService { static const int COMMUNITY_DESCRIPTION_MAX_LENGTH = 500; static const int COMMUNITY_USER_ADJECTIVE_MAX_LENGTH = 16; static const int COMMUNITY_RULES_MAX_LENGTH = 1500; - static const int POST_MAX_LENGTH = 1120; - static const int POST_COMMENT_MAX_LENGTH = 560; + static const int POST_MAX_LENGTH = 5000; + static const int POST_COMMENT_MAX_LENGTH = 1500; static const int PASSWORD_MIN_LENGTH = 10; static const int PASSWORD_MAX_LENGTH = 100; static const int CIRCLE_MAX_LENGTH = 100; @@ -30,7 +30,7 @@ class ValidationService { static const int MODERATED_OBJECT_DESCRIPTION_MAX_LENGTH = 1000; static const int PROFILE_NAME_MIN_LENGTH = 1; static const int PROFILE_LOCATION_MAX_LENGTH = 64; - static const int PROFILE_BIO_MAX_LENGTH = 150; + static const int PROFILE_BIO_MAX_LENGTH = 1000; static const int POST_IMAGE_MAX_SIZE = 20971520; static const int AVATAR_IMAGE_MAX_SIZE = 10485760; static const int COVER_IMAGE_MAX_SIZE = 10485760; diff --git a/lib/widgets/icon.dart b/lib/widgets/icon.dart index 5617e1ba7..c785eeaf7 100644 --- a/lib/widgets/icon.dart +++ b/lib/widgets/icon.dart @@ -222,6 +222,8 @@ class OBIcons { static const unverify = OBIconData(nativeIcon: Icons.close); static const globalModerator = OBIconData(nativeIcon: Icons.account_balance); static const moderationPenalties = OBIconData(nativeIcon: Icons.flag); + static const send = OBIconData(nativeIcon: Icons.send); + static const arrowDown = OBIconData(nativeIcon: Icons.keyboard_arrow_down); static const success = OBIconData(filename: 'success-icon.png'); static const error = OBIconData(filename: 'error-icon.png'); static const warning = OBIconData(filename: 'warning-icon.png'); diff --git a/lib/widgets/post/post.dart b/lib/widgets/post/post.dart index 69ddffce9..17a7eb696 100644 --- a/lib/widgets/post/post.dart +++ b/lib/widgets/post/post.dart @@ -1,11 +1,9 @@ import 'package:Openbook/models/post.dart'; -import 'package:Openbook/pages/home/bottom_sheets/post_actions.dart'; import 'package:Openbook/widgets/post/widgets/post-actions/post_actions.dart'; import 'package:Openbook/widgets/post/widgets/post-body/post_body.dart'; import 'package:Openbook/widgets/post/widgets/post_circles.dart'; import 'package:Openbook/widgets/post/widgets/post_comments/post_comments.dart'; import 'package:Openbook/widgets/post/widgets/post_header/post_header.dart'; -import 'package:Openbook/widgets/post/widgets/post_is_closed.dart'; import 'package:Openbook/widgets/post/widgets/post_reactions/post_reactions.dart'; import 'package:Openbook/widgets/theming/post_divider.dart'; import 'package:flutter/material.dart'; 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_image.dart b/lib/widgets/post/widgets/post-body/widgets/post_body_image.dart index 599212b5e..264b62fec 100644 --- a/lib/widgets/post/widgets/post-body/widgets/post_body_image.dart +++ b/lib/widgets/post/widgets/post-body/widgets/post_body_image.dart @@ -5,7 +5,6 @@ import 'package:Openbook/widgets/icon.dart'; import 'package:flutter_advanced_networkimage/provider.dart'; import 'package:flutter_advanced_networkimage/transition.dart'; import 'package:flutter/material.dart'; -import 'package:pigment/pigment.dart'; import 'dart:math'; class OBPostBodyImage extends StatelessWidget { 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..541d96be9 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,53 +1,159 @@ import 'package:Openbook/provider.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/models/post.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/scroll_container.dart'; import 'package:Openbook/widgets/theming/actionable_smart_text.dart'; +import 'package:Openbook/widgets/theming/secondary_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 = 1300; + static const int _CUTOFF_OFFSET = 25; + ToastService _toastService; BuildContext _context; + ScrollView _scroll; + bool _expanded; - OBPostBodyText(this._post); + 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; + } @override Widget build(BuildContext context) { _toastService = OpenbookProvider.of(context).toastService; _context = context; + var scrollContainer = (_context + .inheritFromWidgetOfExactType(OBScrollContainer) as OBScrollContainer); + if (scrollContainer != null) _scroll = scrollContainer.scroll; return GestureDetector( - onLongPress: _copyText, - child: Padding( - padding: EdgeInsets.all(20.0), - child: _buildActionablePostText() - ) + onLongPress: _copyText, + 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) { + var maxlength = expanded ? null : _LENGTH_LIMIT - _CUTOFF_OFFSET; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildActionablePostText(postText, post, maxlength: maxlength), + GestureDetector( + onTap: _toggleExpanded, + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OBSecondaryText( + expanded ? "Show less" : "Show more", + size: OBTextSize.medium, + textAlign: TextAlign.start, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox( + width: 10, + ), + OBIcon( + expanded ? OBIcons.arrowUp : OBIcons.arrowDown, + themeColor: OBIconThemeColor.secondaryText, + ) + ], + ), + ), + ), + ], + ); + } + + 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, + ); + } + } + + void _toggleExpanded() { + if (!_expanded) { + _heightCollapsed = context.size.height; + if (_hasScrollController()) { + _minScrollOffset = _scroll.controller.position.minScrollExtent; + _maxScrollOffset = _scroll.controller.position.maxScrollExtent; + } + } else { + _heightExpanded = context.size.height; + } + + setState(() { + _doScroll = _expanded; + _expanded = !_expanded; + + if (_doScroll && _hasScrollController()) { + var scrollOffset = + _scroll.controller.offset - (_heightExpanded - _heightCollapsed); + _scroll.controller + .jumpTo(scrollOffset.clamp(_minScrollOffset, _maxScrollOffset)); + _doScroll = false; + } }); } + bool _hasScrollController() { + return _scroll != null && _scroll.controller != null; + } + void _copyText() { - Clipboard.setData(ClipboardData(text: _post.text)); - _toastService.toast(message: 'Text copied!', context: _context, type: ToastType.info); + Clipboard.setData(ClipboardData(text: widget._post.text)); + _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; + } +} 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 0bc76e047..0f86ff878 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);