Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for posting as a different user #1159

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions lib/account/utils/profiles.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/account/widgets/profile_modal_body.dart';

void showProfileModalSheet(BuildContext context, {bool showLogoutDialog = false}) {
Future<void> showProfileModalSheet(
BuildContext context, {
bool showLogoutDialog = false,
bool quickSelectMode = false,
String? customHeading,
bool reloadOnSwitch = true,
}) async {
AuthBloc authBloc = context.read<AuthBloc>();
ThunderBloc thunderBloc = context.read<ThunderBloc>();

Expand All @@ -23,7 +29,12 @@ void showProfileModalSheet(BuildContext context, {bool showLogoutDialog = false}
],
child: FractionallySizedBox(
heightFactor: 0.8,
child: ProfileModalBody(showLogoutDialog: showLogoutDialog),
child: ProfileModalBody(
showLogoutDialog: showLogoutDialog,
quickSelectMode: quickSelectMode,
customHeading: customHeading,
reloadOnSwitch: reloadOnSwitch,
),
),
);
},
Expand Down
68 changes: 46 additions & 22 deletions lib/account/widgets/profile_modal_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ProfileModalBody extends StatefulWidget {
final bool anonymous;
final bool showLogoutDialog;
micahmo marked this conversation as resolved.
Show resolved Hide resolved
final bool quickSelectMode;
final String? customHeading;
final bool reloadOnSwitch;

const ProfileModalBody({super.key, this.anonymous = false, this.showLogoutDialog = false});
const ProfileModalBody({
super.key,
this.anonymous = false,
this.showLogoutDialog = false,
this.quickSelectMode = false,
this.customHeading,
this.reloadOnSwitch = true,
});

static final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>();

Expand All @@ -46,6 +56,9 @@ class _ProfileModalBodyState extends State<ProfileModalBody> {
child: ProfileSelect(
pushRegister: pushRegister,
showLogoutDialog: widget.showLogoutDialog,
quickSelectMode: widget.quickSelectMode,
customHeading: widget.customHeading,
reloadOnSave: widget.reloadOnSwitch,
),
)
],
Expand All @@ -60,6 +73,9 @@ class _ProfileModalBodyState extends State<ProfileModalBody> {
page = ProfileSelect(
pushRegister: pushRegister,
showLogoutDialog: widget.showLogoutDialog,
quickSelectMode: widget.quickSelectMode,
customHeading: widget.customHeading,
reloadOnSave: widget.reloadOnSwitch,
);
break;

Expand All @@ -80,11 +96,17 @@ class _ProfileModalBodyState extends State<ProfileModalBody> {
class ProfileSelect extends StatefulWidget {
final void Function({bool anonymous}) pushRegister;
final bool showLogoutDialog;
final bool quickSelectMode;
final String? customHeading;
final bool reloadOnSave;

const ProfileSelect({
super.key,
required this.pushRegister,
this.showLogoutDialog = false,
this.quickSelectMode = false,
this.customHeading,
this.reloadOnSave = true,
});

@override
Expand Down Expand Up @@ -126,7 +148,7 @@ class _ProfileSelectState extends State<ProfileSelect> {
fetchAccounts();
}

if (anonymousInstances == null) {
if (!widget.quickSelectMode && anonymousInstances == null) {
fetchAnonymousInstances();
}

Expand All @@ -142,37 +164,39 @@ class _ProfileSelectState extends State<ProfileSelect> {
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text(l10n.account(2)),
title: Text(widget.customHeading ?? l10n.account(2)),
centerTitle: false,
scrolledUnderElevation: 0,
pinned: true,
actions: [
IconButton(
icon: const Icon(Icons.person_add),
tooltip: l10n.addAccount,
onPressed: () => widget.pushRegister(),
),
IconButton(
icon: const Icon(Icons.add),
tooltip: l10n.addAnonymousInstance,
onPressed: () => widget.pushRegister(anonymous: true),
),
const SizedBox(width: 12.0),
],
actions: !widget.quickSelectMode
? [
IconButton(
icon: const Icon(Icons.person_add),
tooltip: l10n.addAccount,
onPressed: () => widget.pushRegister(),
),
IconButton(
icon: const Icon(Icons.add),
tooltip: l10n.addAnonymousInstance,
onPressed: () => widget.pushRegister(anonymous: true),
),
const SizedBox(width: 12.0),
]
: [],
),
SliverList.builder(
itemBuilder: (context, index) {
if (index < (accounts?.length ?? 0)) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Material(
color: currentAccountId == accounts![index].account.id ? selectedColor : null,
borderRadius: BorderRadius.circular(50),
child: InkWell(
onTap: (currentAccountId == accounts![index].account.id)
? null
: () {
context.read<AuthBloc>().add(SwitchAccount(accountId: accounts![index].account.id));
context.read<AuthBloc>().add(SwitchAccount(accountId: accounts![index].account.id, reload: widget.reloadOnSave));
context.pop();
},
borderRadius: BorderRadius.circular(50),
Expand Down Expand Up @@ -284,7 +308,7 @@ class _ProfileSelectState extends State<ProfileSelect> {
),
],
),
trailing: (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true)
trailing: !widget.quickSelectMode && (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true)
? (currentAccountId == accounts![index].account.id)
? IconButton(
icon: loggingOutId == accounts![index].account.id
Expand Down Expand Up @@ -329,10 +353,10 @@ class _ProfileSelectState extends State<ProfileSelect> {
),
),
);
} else {
} else if (!widget.quickSelectMode) {
int realIndex = index - (accounts?.length ?? 0);
return Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Material(
color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? selectedColor : null,
borderRadius: BorderRadius.circular(50),
Expand Down Expand Up @@ -462,7 +486,7 @@ class _ProfileSelectState extends State<ProfileSelect> {
),
],
),
trailing: ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1)
trailing: !widget.quickSelectMode && ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1)
? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance)
? IconButton(
icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance),
Expand Down
127 changes: 99 additions & 28 deletions lib/community/pages/create_post_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'package:thunder/account/models/account.dart';
import 'package:thunder/account/utils/profiles.dart';
import 'package:thunder/community/bloc/image_bloc.dart';
import 'package:thunder/community/utils/post_card_action_helpers.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
Expand All @@ -36,7 +37,6 @@ import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/utils/debounce.dart';
import 'package:thunder/utils/image.dart';
import 'package:thunder/utils/instance.dart';
import 'package:thunder/post/utils/navigate_post.dart';

class CreatePostPage extends StatefulWidget {
final int? communityId;
Expand All @@ -61,7 +61,7 @@ class CreatePostPage extends StatefulWidget {
final PostView? postView;

/// Callback function that is triggered whenever the post is successfully created or updated
final Function(PostViewMedia postViewMedia)? onPostSuccess;
final Function(PostViewMedia postViewMedia, bool userChanged)? onPostSuccess;

const CreatePostPage({
super.key,
Expand Down Expand Up @@ -139,6 +139,8 @@ class _CreatePostPageState extends State<CreatePostPage> {

final imageBloc = ImageBloc();

bool userChanged = false;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -297,7 +299,7 @@ class _CreatePostPageState extends State<CreatePostPage> {
child: BlocConsumer<CreatePostCubit, CreatePostState>(
listener: (context, state) {
if (state.status == CreatePostStatus.success && state.postViewMedia != null) {
widget.onPostSuccess?.call(state.postViewMedia!);
widget.onPostSuccess?.call(state.postViewMedia!, userChanged);
Navigator.of(context).pop();
}

Expand Down Expand Up @@ -380,7 +382,21 @@ class _CreatePostPageState extends State<CreatePostPage> {
},
),
const SizedBox(height: 4.0),
const UserIndicator(),
UserSelector(
communityActorId: communityView?.community.actorId,
onCommunityChanged: (CommunityView? cv) {
if (cv == null) {
showSnackbar(l10n.unableToFindCommunityOnInstance);
}

setState(() {
communityId = cv?.community.id;
communityView = cv;
});
_validateSubmission();
},
onUserChanged: () => userChanged = true,
),
const SizedBox(height: 12.0),
TypeAheadField<String>(
suggestionsCallback: (String pattern) async {
Expand Down Expand Up @@ -635,6 +651,79 @@ class _CreatePostPageState extends State<CreatePostPage> {
}
}

/// Creates a widget which displays a preview of the currently selected account, with the ability to change accounts.
///
/// By passing in a [communityActorId], it will attempt to resolve the community to the new user's instance (if changed),
/// and will invoke [onCommunityChanged]. If the community could not be resolved, the callback will pass [null].
class UserSelector extends StatefulWidget {
final String? communityActorId;
final void Function(CommunityView?) onCommunityChanged;
final void Function() onUserChanged;

const UserSelector({
super.key,
required this.communityActorId,
required this.onCommunityChanged,
required this.onUserChanged,
});

@override
State<UserSelector> createState() => _UserSelectorState();
}

class _UserSelectorState extends State<UserSelector> {
@override
Widget build(BuildContext context) {
final AppLocalizations l10n = AppLocalizations.of(context)!;

return Transform.translate(
offset: const Offset(-8, 0),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(50)),
onTap: () async {
final Account? originalUser = context.read<AuthBloc>().state.account;

await showProfileModalSheet(
context,
quickSelectMode: true,
customHeading: l10n.selectAccountToPostAs,
reloadOnSwitch: false,
);

// Wait slightly longer than the duration that is waited in the account switcher logic.
await Future.delayed(const Duration(milliseconds: 1500));

if (context.mounted) {
Account? newUser = context.read<AuthBloc>().state.account;

if (originalUser != null && newUser != null && originalUser.id != newUser.id) {
// The user changed. Reload the widget.
setState(() {});
widget.onUserChanged();

//If there is a selected community, see if we can resolve it to the new user's instance.
if (widget.communityActorId?.isNotEmpty == true) {
CommunityView? resolvedCommunity;
try {
final ResolveObjectResponse resolveObjectResponse = await LemmyApiV3(newUser.instance!).run(ResolveObject(q: widget.communityActorId!));
resolvedCommunity = resolveObjectResponse.community;
} catch (e) {
// We'll just return null if we can't find it.
}
widget.onCommunityChanged(resolvedCommunity);
}
}
}
},
child: const Padding(
padding: EdgeInsets.only(left: 8, top: 4, bottom: 4),
child: UserIndicator(),
),
),
);
}
}

/// Creates a widget which displays a preview of a pre-selected language, with the ability to change the selected language
///
/// Passing in [languageId] will set the initial state of the widget to display that given language.
Expand Down Expand Up @@ -735,17 +824,6 @@ class CommunitySelector extends StatefulWidget {
}

class _CommunitySelectorState extends State<CommunitySelector> {
int? _communityId;
CommunityView? _communityView;

@override
void initState() {
super.initState();

_communityId = widget.communityId;
_communityView = widget.communityView;
}

@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
Expand All @@ -758,30 +836,23 @@ class _CommunitySelectorState extends State<CommunitySelector> {
showCommunityInputDialog(
context,
title: l10n.community,
onCommunitySelected: (cv) {
setState(() {
_communityId = cv.community.id;
_communityView = cv;
});

widget.onCommunitySelected(cv);
},
onCommunitySelected: widget.onCommunitySelected,
);
},
borderRadius: const BorderRadius.all(Radius.circular(50)),
child: Padding(
padding: const EdgeInsets.only(left: 8, top: 12, bottom: 12),
padding: const EdgeInsets.only(left: 8, top: 4, bottom: 4),
child: Row(
children: [
CommunityAvatar(community: _communityView?.community, radius: 16),
CommunityAvatar(community: widget.communityView?.community, radius: 16),
const SizedBox(width: 12),
_communityId != null
widget.communityId != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${_communityView?.community.title} '),
Text('${widget.communityView?.community.title} '),
FutureBuilder(
future: getLemmyCommunity(_communityView?.community.actorId ?? ''),
future: getLemmyCommunity(widget.communityView?.community.actorId ?? ''),
builder: (context, snapshot) {
return Text(
snapshot.data ?? '',
Expand Down
9 changes: 8 additions & 1 deletion lib/core/auth/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: account.jwt));
bool downvotesEnabled = getSiteResponse.siteView.localSite.enableDownvotes;

return emit(state.copyWith(status: AuthStatus.success, account: account, isLoggedIn: true, downvotesEnabled: downvotesEnabled, getSiteResponse: getSiteResponse));
return emit(state.copyWith(
status: AuthStatus.success,
account: account,
isLoggedIn: true,
downvotesEnabled: downvotesEnabled,
getSiteResponse: getSiteResponse,
reload: event.reload,
));
});

// This event should be triggered during the start of the app, or when there is a change in the active account
Expand Down
Loading
Loading