From 5515fde8801015d12df35483a70216f7b061a13f Mon Sep 17 00:00:00 2001 From: zoe Date: Tue, 6 Sep 2022 23:42:09 +0200 Subject: [PATCH] threads view --- lib/business_logic/account/account.dart | 4 +- lib/business_logic/posts/posts.dart | 44 +++++ lib/business_logic/posts/sdfsdfdsf.json | 0 lib/business_logic/timeline/timeline.dart | 5 + lib/dialogues/full_post_view.dart | 138 ++++++++++++++++ .../profile_view.dart | 8 +- lib/pages/notifications/single_notif.dart | 2 +- lib/partials/post.dart | 152 +++++++++++------- lib/partials/post_options.dart | 11 ++ 9 files changed, 296 insertions(+), 68 deletions(-) create mode 100644 lib/business_logic/posts/posts.dart create mode 100644 lib/business_logic/posts/sdfsdfdsf.json create mode 100644 lib/dialogues/full_post_view.dart rename lib/{pages/profile_view => dialogues}/profile_view.dart (98%) diff --git a/lib/business_logic/account/account.dart b/lib/business_logic/account/account.dart index d52b93a..9a12378 100644 --- a/lib/business_logic/account/account.dart +++ b/lib/business_logic/account/account.dart @@ -122,14 +122,14 @@ Future> getRelationship( ); final response = await http.get(uri, headers: headers); - if (response.statusCode == 200) { + if (response.statusCode == 200 && jsonDecode(response.body).isNotEmpty) { return { response.statusCode: RelationshipModel.fromJson(jsonDecode(response.body)[0], identityName) }; } - return {404: null}; + return {response.statusCode: null}; } Future> searchModel( diff --git a/lib/business_logic/posts/posts.dart b/lib/business_logic/posts/posts.dart new file mode 100644 index 0000000..6659b67 --- /dev/null +++ b/lib/business_logic/posts/posts.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:loris/business_logic/timeline/timeline.dart'; +import 'package:loris/global.dart' as global; +import 'package:http/http.dart' as http; + +class PostContext { + final List ancestors; + final List descendants; + + PostContext({required this.ancestors, required this.descendants}); +} + +// first returns ancestors, then decendants +Future> getContextForPost(PostModel model) async { + final identity = global.settings!.identities[model.identity]!; + var headers = identity.getAuthHeaders(); + headers.addAll(global.defaultHeaders); + String id = model.reblogId ?? model.id; + final uri = Uri( + scheme: "https", + host: identity.instanceUrl, + path: "/api/v1/statuses/$id/context"); + + final r = await http.get(uri, headers: headers); + if (r.statusCode != 200) { + return MapEntry(r.statusCode, null); + } + + final json = jsonDecode(r.body); + List ancestors = json["ancestors"]!; + List descendants = json["descendants"]!; + + return MapEntry( + r.statusCode, + PostContext( + ancestors: ancestors + .map((e) => PostModel.fromJson(e, model.identity)) + .toList(), + descendants: descendants + .map((e) => PostModel.fromJson(e, model.identity)) + .toList(), + )); +} diff --git a/lib/business_logic/posts/sdfsdfdsf.json b/lib/business_logic/posts/sdfsdfdsf.json new file mode 100644 index 0000000..e69de29 diff --git a/lib/business_logic/timeline/timeline.dart b/lib/business_logic/timeline/timeline.dart index 1616c2a..0af6c57 100644 --- a/lib/business_logic/timeline/timeline.dart +++ b/lib/business_logic/timeline/timeline.dart @@ -83,6 +83,8 @@ class PostModel implements Comparable { late bool reblogged; late AccountModel account; late AccountModel? rebloggedBy; + late PostModel? reblog; + late String? inReplyTo; late List attachments; late List mentions = []; // exists if post is a reblog @@ -97,8 +99,10 @@ class PostModel implements Comparable { rebloggedBy = AccountModel.fromJson(json["account"], identity); json = json["reblog"]; reblogId = json["id"]; + reblog = PostModel.fromJson(json, identity); } else { rebloggedBy = null; + reblog = null; } originalId = json["id"]; uri = json["uri"] as String; @@ -110,6 +114,7 @@ class PostModel implements Comparable { spoilerText = json["spoiler_text"] as String; favourited = json["favourited"] as bool; reblogged = json["reblogged"] as bool; + inReplyTo = json["in_reply_to_id"]; account = AccountModel.fromJson(json["account"], identity); attachments = []; List jsonAttachmentList = json["media_attachments"]; diff --git a/lib/dialogues/full_post_view.dart b/lib/dialogues/full_post_view.dart new file mode 100644 index 0000000..3c0bf73 --- /dev/null +++ b/lib/dialogues/full_post_view.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:loris/business_logic/posts/posts.dart'; +import 'package:loris/business_logic/timeline/timeline.dart'; +import 'package:loris/global.dart' as global; + +import '../partials/post.dart'; + +class FullPostView extends StatefulWidget { + const FullPostView({ + super.key, + required this.originPostModel, + }); + final PostModel originPostModel; + + @override + State createState() => _FullPostViewState(); +} + +class _FullPostViewState extends State { + bool loading = false; + @override + Widget build(BuildContext context) { + return SimpleDialog( + children: [ + Container( + constraints: global.getConstraints(context), + width: global.getWidth(context), + child: SingleChildScrollView( + child: SingleFullPostDisplay( + level: 0, + model: widget.originPostModel.reblog ?? widget.originPostModel, + ), + ), + ) + ], + ); + } +} + +class SingleFullPostDisplay extends StatefulWidget { + const SingleFullPostDisplay({ + super.key, + required this.level, + required this.model, + this.toBeDistributed, + }); + final int level; + final PostModel model; + final List? toBeDistributed; + + @override + State createState() => _SingleFullPostDisplayState(); +} + +class _SingleFullPostDisplayState extends State { + bool loading = true; + List ancestors = []; + List descendants = []; + + @override + void initState() { + if (widget.level == 0) { + loadPosts(); + } else { + setState(() { + loading = false; + }); + } + super.initState(); + } + + void loadPosts() async { + final r = await getContextForPost(widget.model); + if (r.value != null) { + setState(() { + if (widget.level == 0) { + ancestors = r.value!.ancestors; + } + descendants = r.value!.descendants; + }); + } + if (mounted) { + setState(() { + loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + List toBeDistributed = widget.toBeDistributed ?? []; + List ancestorWidgets = ancestors + .map( + (e) => Post(model: e), + ) + .toList(); + + List descendantsWidgets = []; + + if (widget.toBeDistributed == null) { + for (var element in descendants) { + toBeDistributed.add(element); + if (element.id == widget.model.id) {} + } + } + + for (var element in toBeDistributed) { + if (element.inReplyTo == widget.model.id) { + descendantsWidgets.add(SingleFullPostDisplay( + level: widget.level + 1, + model: element, + toBeDistributed: toBeDistributed, + )); + } + } + + List c = []; + c.addAll(ancestorWidgets); + c.add(Post(model: widget.model)); + c.addAll(descendantsWidgets); + + return Container( + padding: const EdgeInsets.fromLTRB(8, 0, 0, 0), + decoration: BoxDecoration( + border: widget.level == 0 + ? const Border() + : Border( + left: BorderSide( + width: 4, + color: widget.level.isEven + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary, + ), + ), + ), + child: Column(children: c)); + } +} diff --git a/lib/pages/profile_view/profile_view.dart b/lib/dialogues/profile_view.dart similarity index 98% rename from lib/pages/profile_view/profile_view.dart rename to lib/dialogues/profile_view.dart index 03ea4c0..67bbf72 100644 --- a/lib/pages/profile_view/profile_view.dart +++ b/lib/dialogues/profile_view.dart @@ -46,7 +46,7 @@ class _ProfileViewState extends State { } } - void addIdentity(String identityName, int i) async { + Future addIdentity(String identityName, int i) async { final m = await searchModel(identityName, widget.model.url); if (m.values.first != null) { if (mounted) { @@ -56,7 +56,7 @@ class _ProfileViewState extends State { } await addRelationship(m.values.first!); } - if (i >= global.settings!.identities.length - 1) { + if (i >= global.settings!.identities.length - 2) { if (mounted) { setState(() { loading = false; @@ -101,12 +101,10 @@ class _ProfileViewState extends State { global.settings!.identities.entries, (element) async { if (element.key == widget.model.identity) { - i++; - addIdentity(element.key, i); return; } + await addIdentity(element.key, i); i++; - addIdentity(element.key, i); }, ); } diff --git a/lib/pages/notifications/single_notif.dart b/lib/pages/notifications/single_notif.dart index ed04cbf..17ca16b 100644 --- a/lib/pages/notifications/single_notif.dart +++ b/lib/pages/notifications/single_notif.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:loris/business_logic/notifications/notifs.dart'; -import 'package:loris/pages/profile_view/profile_view.dart'; +import 'package:loris/dialogues/profile_view.dart'; import 'package:loris/partials/post.dart'; import '../../global.dart' as global; diff --git a/lib/partials/post.dart b/lib/partials/post.dart index ac8b6f0..2388a7d 100644 --- a/lib/partials/post.dart +++ b/lib/partials/post.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:localization/localization.dart'; import 'package:loris/business_logic/account/account.dart'; import 'package:loris/business_logic/timeline/media.dart'; +import 'package:loris/dialogues/full_post_view.dart'; import 'package:loris/dialogues/makepost.dart'; -import 'package:loris/pages/profile_view/profile_view.dart'; +import 'package:loris/dialogues/profile_view.dart'; import 'package:loris/partials/interaction_button.dart'; import 'package:loris/partials/media_attachment.dart'; import 'package:loris/partials/post_options.dart'; @@ -35,12 +36,16 @@ class _PostState extends State { List c = []; c.addAll([ DisplayName(account: widget.model.account), - PostBody( - sensitive: widget.model.sensitive, - content: widget.model.content, - spoilerText: widget.model.spoilerText, - media: widget.model.attachments, - forceShow: !widget.hideSensitive, + SizedBox( + width: double.maxFinite, + child: PostBody( + model: widget.model, + sensitive: widget.model.sensitive, + content: widget.model.content, + spoilerText: widget.model.spoilerText, + media: widget.model.attachments, + forceShow: !widget.hideSensitive, + ), ), ]); if (!widget.hideActionBar) { @@ -192,13 +197,17 @@ class PostBody extends StatefulWidget { required this.content, required this.media, required this.forceShow, + this.openInBrowser = false, Key? key, + required this.model, }) : super(key: key); final String content; final String spoilerText; final bool sensitive; final bool forceShow; final List media; + final bool openInBrowser; + final tl.PostModel model; @override State createState() => _PostBodyState(); @@ -217,62 +226,72 @@ class _PostBodyState extends State { @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(0, 6, 0, 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Visibility( - visible: widget.forceShow ? false : widget.sensitive, - child: Column( - children: [ - SelectableText.rich( - TextSpan( - children: [ - WidgetSpan( - child: ElevatedButton.icon( - onPressed: () { - setState(() { - visible = !visible; - if (visible) { - cwButtonIcon = const Icon(Icons.visibility_off); - cwButtonText = "hide".i18n(); - } else { - cwButtonText = "show".i18n(); - cwButtonIcon = const Icon(Icons.visibility); - } - }); - }, - icon: cwButtonIcon, - label: Text(cwButtonText), - ), + return InkWell( + onTap: () => showDialog( + context: context, + builder: (context) => FullPostView(originPostModel: widget.model), + ), + child: Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(0, 6, 0, 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: widget.forceShow ? false : widget.sensitive, + child: Column( + children: [ + SizedBox( + width: double.maxFinite, + child: SelectableText.rich( + TextSpan( + children: [ + WidgetSpan( + child: ElevatedButton.icon( + onPressed: () { + setState(() { + visible = !visible; + if (visible) { + cwButtonIcon = + const Icon(Icons.visibility_off); + cwButtonText = "hide".i18n(); + } else { + cwButtonText = "show".i18n(); + cwButtonIcon = const Icon(Icons.visibility); + } + }); + }, + icon: cwButtonIcon, + label: Text(cwButtonText), + ), + ), + const WidgetSpan( + child: SizedBox( + width: 8, + )), + TextSpan(text: widget.spoilerText), + ], ), - const WidgetSpan( - child: SizedBox( - width: 8, - )), - TextSpan(text: widget.spoilerText), - ], + ), ), - ), - const SizedBox( - height: 8, - ) - ], + const SizedBox( + height: 8, + ) + ], + ), ), - ), - Visibility( - visible: visible || widget.forceShow, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PostTextRenderer(input: widget.content), - MediaAttachments(models: widget.media), - ], + Visibility( + visible: visible || widget.forceShow, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PostTextRenderer(input: widget.content), + MediaAttachments(models: widget.media), + ], + ), ), - ), - ], + ], + ), ), ); } @@ -320,6 +339,19 @@ class _PostActionBarState extends State { type: interactions.InteractionType.favorite, ), ), + Expanded( + flex: 20, + child: IconButton( + tooltip: "show-in-full".i18n(), + onPressed: () { + showDialog( + context: context, + builder: (context) => FullPostView( + originPostModel: widget.model, + ), + ); + }, + icon: const Icon(Icons.open_in_full))), Expanded( flex: 20, child: IconButton( @@ -329,7 +361,7 @@ class _PostActionBarState extends State { }, icon: const Icon(Icons.more_horiz), ), - ) + ), ], ); } diff --git a/lib/partials/post_options.dart b/lib/partials/post_options.dart index 8547ce4..186801d 100644 --- a/lib/partials/post_options.dart +++ b/lib/partials/post_options.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:localization/localization.dart'; import 'package:loris/business_logic/interactions/interactions.dart'; import 'package:loris/business_logic/timeline/timeline.dart'; +import 'package:loris/dialogues/full_post_view.dart'; import 'package:loris/partials/interaction_button.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:clipboard/clipboard.dart'; @@ -49,6 +50,16 @@ class _PostOptionsState extends State { const SizedBox( height: 24, ), + TextButton.icon( + onPressed: () => showDialog( + context: context, + builder: (context) => + FullPostView(originPostModel: widget.model), + ), + icon: const Icon( + Icons.open_in_full, + ), + label: Text("show-in-full".i18n())), TextButton.icon( onPressed: () async { FlutterClipboard.copy(widget.model.uri);