import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:localization/localization.dart'; import 'package:loris/business_logic/account/account.dart'; import 'package:loris/business_logic/settings.dart'; import 'package:loris/business_logic/timeline/timeline.dart'; import 'package:loris/partials/loadingbox.dart'; import 'package:loris/partials/post.dart'; import 'package:loris/partials/post_text_renderer.dart'; import 'package:loris/global.dart' as global; import 'package:loris/partials/thread.dart'; import 'package:loris/themes/themes.dart' as themes; class ProfileView extends StatefulWidget { const ProfileView({ super.key, required this.model, }); final AccountModel model; @override State createState() => _ProfileViewState(); } class _ProfileViewState extends State { final StreamController> _relationshipStream = StreamController(); Map identities = {}; Map relationships = {}; String activeIdentity = ""; bool loading = true; List threads = []; String? maxid; final ScrollController _scrollController = ScrollController(); bool loadingPosts = false; Future addRelationship(AccountModel m) async { final r = await getRelationship(m.identity, m.id); if (r.keys.first != 200) { return; } else if (mounted) { setState(() { relationships.addAll({ m.identity: r.values.first, }); }); } } Future addIdentity(String identityName, int i) async { final m = await searchModel(identityName, widget.model.url); if (m.values.first != null) { if (mounted) { setState(() { identities.addAll({identityName: m.values.first!}); }); } await addRelationship(m.values.first!); } if (i >= global.settings!.identities.length - 2) { if (mounted) { setState(() { loading = false; }); } } } void loadPosts() async { if (!loadingPosts) { loadingPosts = true; if (threads.isNotEmpty) { maxid = threads.last.posts.last.id; } final t = await getPostsForAccount(identities[activeIdentity]!, maxid); if (t.value == null) { return; } if (mounted) { setState(() { threads.addAll(t.value!); }); } loadingPosts = false; } } void reloadPosts() { _scrollController.animateTo(0, duration: const Duration(seconds: 1), curve: Curves.ease); setState(() { maxid = null; threads = []; }); loadPosts(); } void update() async { int i = 0; await Future.forEach>( global.settings!.identities.entries, (element) async { if (element.key == widget.model.identity) { return; } await addIdentity(element.key, i); i++; }, ); } @override void dispose() { _scrollController.dispose(); _relationshipStream.close(); super.dispose(); } @override void initState() { if (global.settings!.identities.length == 1) { loading = false; } activeIdentity = widget.model.identity; identities.addAll({widget.model.identity: widget.model}); addRelationship(widget.model); update(); loadPosts(); _scrollController.addListener(() { if (_scrollController.offset >= _scrollController.position.maxScrollExtent - MediaQuery.of(context).size.height && !_scrollController.position.outOfRange) { loadPosts(); } }); _relationshipStream.stream.listen((event) { setState(() { relationships.addEntries([event]); }); }); super.initState(); } @override Widget build(BuildContext context) { List> dmenuItems = []; identities.forEach((key, value) { dmenuItems.add( DropdownMenuItem( alignment: Alignment.center, value: key, child: Text( key, style: Theme.of(context).textTheme.bodyMedium, ), ), ); }); final profileViewDisplay = Padding( padding: const EdgeInsets.all(0), child: ProfileViewDisplay( accountModel: identities[activeIdentity]!, relationshipModel: relationships[activeIdentity], relationshipStream: _relationshipStream, ), ); return BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10, tileMode: TileMode.mirror), child: SimpleDialog( alignment: Alignment.center, title: DisplayName( account: identities[activeIdentity]!, openInBrowser: true, ), contentPadding: const EdgeInsets.all(0), children: [ Container( constraints: global.getConstraints(context), child: Column(children: [ loading ? Column( children: [ SelectableText( "${"found-account-on".i18n()} ${identities.length} ${identities.length <= 1 ? "instance" : "instances".i18n()}"), const LinearProgressIndicator(), ], ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ DropdownButtonHideUnderline( child: DropdownButton( isExpanded: false, alignment: Alignment.center, iconEnabledColor: Theme.of(context).colorScheme.onSurface, value: activeIdentity, items: dmenuItems, onChanged: (value) { setState(() { activeIdentity = value.toString(); }); reloadPosts(); }, ), ), ], ), SizedBox( // constraints: global.getConstraints(context), width: global.getWidth(context), height: MediaQuery.of(context).size.height * 2 / 3, child: ListView.separated( separatorBuilder: (context, index) => const Divider( color: Colors.transparent, height: themes.defaultSeperatorHeight, ), controller: _scrollController, itemCount: threads.length + 2, itemBuilder: (context, index) { if (index == 0) { return profileViewDisplay; } else if (index > 0 && index <= threads.length) { return Padding( padding: const EdgeInsets.symmetric( horizontal: themes.defaultSeperatorHeight * 2), child: Thread( model: threads[index - 1], constrained: false, ), ); } return const LoadingBox(); }, ), ), ]), ), ], ), ); } } class AccountInteractionButtons extends StatelessWidget { const AccountInteractionButtons({ super.key, required this.account, required this.relationship, required this.stream, }); final AccountModel account; final RelationshipModel relationship; final StreamController stream; @override Widget build(BuildContext context) { return Wrap( children: [ AccountInteractionButton( accountModel: account, relationshipModel: relationship, type: AccountInteractionTypes.follow, stream: stream, ), AccountInteractionButton( accountModel: account, relationshipModel: relationship, type: AccountInteractionTypes.block, stream: stream, ), ], ); } } class StatusIndicators extends StatelessWidget { const StatusIndicators({ super.key, required this.model, this.relationship, }); final AccountModel model; final RelationshipModel? relationship; List getTextWithIcon(IconData icon, String t) { return [ WidgetSpan( child: Icon(icon), alignment: PlaceholderAlignment.middle, ), TextSpan( text: t, ) ]; } @override Widget build(BuildContext context) { List c = []; if (relationship == null) { c.add(const WidgetSpan(child: LinearProgressIndicator())); } else { // follow relationship if (relationship!.followedBy && relationship!.following) { c.addAll(getTextWithIcon(Icons.group, "you-are-mufos".i18n())); } else if (relationship!.followedBy) { c.addAll(getTextWithIcon(Icons.group, "they-follow-you".i18n())); } else if (relationship!.following) { c.addAll(getTextWithIcon(Icons.group, "you-follow-them".i18n())); } else { c.addAll(getTextWithIcon( Icons.group, "you-do-not-follow-each-other".i18n())); } if (relationship!.requested) { c.addAll( getTextWithIcon(Icons.group_add, "pending-follow-request".i18n())); } } // account is a bot if (model.bot != null) { if (model.bot!) { c.addAll(getTextWithIcon(Icons.smart_toy, "user-is-bot".i18n())); } } return SelectableText.rich( TextSpan(children: c), ); } } class ProfileViewDisplay extends StatelessWidget { const ProfileViewDisplay({ super.key, required this.accountModel, this.relationshipModel, required this.relationshipStream, }); final AccountModel accountModel; final RelationshipModel? relationshipModel; final StreamController relationshipStream; static const d = SizedBox( height: 8, ); @override Widget build(BuildContext context) { List c = [ if (!(accountModel.header.endsWith("/headers/original/missing.png") || (accountModel.header.endsWith("default_header.png")))) Image.network( fit: BoxFit.fitWidth, accountModel.header, errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(), ), Padding( padding: themes.defaultMargins, child: StatusIndicators( model: accountModel, relationship: relationshipModel, ), ), d, Padding( padding: themes.defaultMargins, child: PostTextRenderer( input: accountModel.note, identityName: accountModel.identity, emoji: accountModel.emojis, ), ), if (relationshipModel != null) Padding( padding: const EdgeInsets.all(themes.defaultSeperatorHeight), child: AccountInteractionButtons( account: accountModel, relationship: relationshipModel!, stream: relationshipStream, ), ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: c, ); } } class AccountInteractionButton extends StatefulWidget { const AccountInteractionButton({ super.key, required this.type, required this.relationshipModel, required this.accountModel, required this.stream, }); final AccountInteractionTypes type; final RelationshipModel relationshipModel; final AccountModel accountModel; final StreamController stream; @override State createState() => AccountInteractionButtonState(); } class AccountInteractionButtonState extends State { bool active() { switch (widget.type) { case AccountInteractionTypes.follow: return widget.relationshipModel.following; case AccountInteractionTypes.block: return widget.relationshipModel.blocking; case AccountInteractionTypes.mute: return widget.relationshipModel.muting; } } @override Widget build(BuildContext context) { return TextButton.icon( onPressed: () async { final r = await performInteraction( widget.relationshipModel.identity, widget.accountModel.id, widget.type, active(), ); if (r.value == null) { return; } widget.stream .add(MapEntry(widget.relationshipModel.identity, r.value!)); }, icon: Icon(widget.type.icon), label: Text( active() ? widget.type.revokeSlug.i18n() : widget.type.slug.i18n(), ), ); } }