Account activity redesign (#662)
* Refactor-all-the-things version of the fix for issue #573 * Migrate SpanUtils to kotlin because why not * Minimal fix for issue #573 * Add tests for compose spanning * Clean up code suggestions * Make FakeSpannable.getSpans implementation less awkward * Add secondary validation pass for urls * Address code review feedback * Fixup type filtering in FakeSpannable again * Make all mentions in compose activity use the default link color * new layout for AccountActivity * fix the light theme * convert AccountActivity to Kotlin * introduce AccountViewModel * Merge branch 'master' into account-activity-redesign # Conflicts: # app/src/main/java/com/keylesspalace/tusky/AccountActivity.java * add Bot badge to profile * parse custom emojis in usernames * add possibility to cancel follow request * add third tab on profiles * add account fields to profile * add support for moved accounts * set click listener on account moved view * fix tests * use 24dp as statusbar size * add ability to hide reblogs from followed accounts * add button to edit own account to AccountActivity * set toolbar top margin programmatically * fix crash * add shadow behind statusbar * introduce ViewExtensions to clean up code * move code out of offsetChangedListener for perf reasons * clean up stuff * add error handling * improve type safety * fix ConstraintLayout warning * remove unneeded ressources * fix event dispatching * fix crash in event handling * set correct emoji on title * improve some things * wrap follower/foillowing/status views
This commit is contained in:
parent
c450af7b0d
commit
63f9d99390
60 changed files with 1422 additions and 978 deletions
|
@ -69,8 +69,7 @@ public class AboutActivity extends BaseActivity implements Injectable {
|
|||
}
|
||||
|
||||
private void viewAccount(String id) {
|
||||
Intent intent = new Intent(this, AccountActivity.class);
|
||||
intent.putExtra("id", id);
|
||||
Intent intent = AccountActivity.getIntent(this, id);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,717 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.design.widget.CollapsingToolbarLayout;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent;
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter;
|
||||
import com.keylesspalace.tusky.util.Assert;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.pkmmte.view.CircularImageView;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public final class AccountActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||
HasSupportFragmentInjector {
|
||||
private static final String TAG = "AccountActivity"; // logging tag
|
||||
|
||||
private enum FollowState {
|
||||
NOT_FOLLOWING,
|
||||
FOLLOWING,
|
||||
REQUESTED,
|
||||
}
|
||||
|
||||
@Inject
|
||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||
@Inject
|
||||
public EventHub appstore;
|
||||
|
||||
private String accountId;
|
||||
private FollowState followState;
|
||||
private boolean blocking;
|
||||
private boolean muting;
|
||||
private boolean isSelf;
|
||||
private Account loadedAccount;
|
||||
|
||||
private CircularImageView avatar;
|
||||
private ImageView header;
|
||||
private FloatingActionButton floatingBtn;
|
||||
private Button followBtn;
|
||||
private TextView followsYouView;
|
||||
private TabLayout tabLayout;
|
||||
private ImageView accountLockedView;
|
||||
private View container;
|
||||
private TextView followersTextView;
|
||||
private TextView followingTextView;
|
||||
private TextView statusesTextView;
|
||||
|
||||
private boolean hideFab;
|
||||
private int oldOffset;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_account);
|
||||
|
||||
avatar = findViewById(R.id.account_avatar);
|
||||
header = findViewById(R.id.account_header);
|
||||
floatingBtn = findViewById(R.id.floating_btn);
|
||||
followBtn = findViewById(R.id.follow_btn);
|
||||
followsYouView = findViewById(R.id.account_follows_you);
|
||||
tabLayout = findViewById(R.id.tab_layout);
|
||||
accountLockedView = findViewById(R.id.account_locked);
|
||||
container = findViewById(R.id.activity_account);
|
||||
followersTextView = findViewById(R.id.followers_tv);
|
||||
followingTextView = findViewById(R.id.following_tv);
|
||||
statusesTextView = findViewById(R.id.statuses_btn);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
accountId = savedInstanceState.getString("accountId");
|
||||
followState = (FollowState) savedInstanceState.getSerializable("followState");
|
||||
blocking = savedInstanceState.getBoolean("blocking");
|
||||
muting = savedInstanceState.getBoolean("muting");
|
||||
} else {
|
||||
Intent intent = getIntent();
|
||||
accountId = intent.getStringExtra("id");
|
||||
followState = FollowState.NOT_FOLLOWING;
|
||||
blocking = false;
|
||||
muting = false;
|
||||
}
|
||||
loadedAccount = null;
|
||||
|
||||
// Setup the toolbar.
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(null);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
hideFab = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fabHide", false);
|
||||
|
||||
// Add a listener to change the toolbar icon color when it enters/exits its collapsed state.
|
||||
AppBarLayout appBarLayout = findViewById(R.id.account_app_bar_layout);
|
||||
final CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar);
|
||||
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
|
||||
@AttrRes
|
||||
int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed;
|
||||
|
||||
@Override
|
||||
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
|
||||
@AttrRes int attribute;
|
||||
if (collapsingToolbar.getHeight() + verticalOffset
|
||||
< 2 * ViewCompat.getMinimumHeight(collapsingToolbar)) {
|
||||
|
||||
toolbar.setTitleTextColor(ThemeUtils.getColor(AccountActivity.this,
|
||||
android.R.attr.textColorPrimary));
|
||||
toolbar.setSubtitleTextColor(ThemeUtils.getColor(AccountActivity.this,
|
||||
android.R.attr.textColorSecondary));
|
||||
|
||||
attribute = R.attr.account_toolbar_icon_tint_collapsed;
|
||||
} else {
|
||||
toolbar.setTitleTextColor(Color.TRANSPARENT);
|
||||
toolbar.setSubtitleTextColor(Color.TRANSPARENT);
|
||||
|
||||
attribute = R.attr.account_toolbar_icon_tint_uncollapsed;
|
||||
}
|
||||
if (attribute != priorAttribute) {
|
||||
priorAttribute = attribute;
|
||||
Context context = toolbar.getContext();
|
||||
ThemeUtils.setDrawableTint(context, toolbar.getNavigationIcon(), attribute);
|
||||
ThemeUtils.setDrawableTint(context, toolbar.getOverflowIcon(), attribute);
|
||||
}
|
||||
|
||||
if (floatingBtn != null && hideFab && !isSelf && !blocking) {
|
||||
if (verticalOffset > oldOffset) {
|
||||
floatingBtn.show();
|
||||
}
|
||||
if (verticalOffset < oldOffset) {
|
||||
floatingBtn.hide();
|
||||
}
|
||||
}
|
||||
oldOffset = verticalOffset;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialise the default UI states.
|
||||
floatingBtn.hide();
|
||||
followBtn.setVisibility(View.GONE);
|
||||
followsYouView.setVisibility(View.GONE);
|
||||
|
||||
// Obtain information to fill out the profile.
|
||||
obtainAccount();
|
||||
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
|
||||
if (accountId.equals(activeAccount.getAccountId())) {
|
||||
isSelf = true;
|
||||
} else {
|
||||
isSelf = false;
|
||||
obtainRelationships();
|
||||
}
|
||||
|
||||
// Setup the tabs and timeline pager.
|
||||
AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(),
|
||||
accountId);
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_statuses),
|
||||
getString(R.string.title_media)
|
||||
};
|
||||
adapter.setPageTitles(pageTitles);
|
||||
final ViewPager viewPager = findViewById(R.id.pager);
|
||||
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
||||
viewPager.setPageMargin(pageMargin);
|
||||
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||
R.drawable.tab_page_margin_dark);
|
||||
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(0);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
View.OnClickListener accountListClickListener = v -> {
|
||||
AccountListActivity.Type type;
|
||||
switch (v.getId()) {
|
||||
case R.id.followers_tv:
|
||||
type = AccountListActivity.Type.FOLLOWERS;
|
||||
break;
|
||||
case R.id.following_tv:
|
||||
type = AccountListActivity.Type.FOLLOWING;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
Intent intent = AccountListActivity.newIntent(AccountActivity.this, type,
|
||||
accountId);
|
||||
startActivity(intent);
|
||||
};
|
||||
followersTextView.setOnClickListener(accountListClickListener);
|
||||
followingTextView.setOnClickListener(accountListClickListener);
|
||||
|
||||
statusesTextView.setOnClickListener(v -> {
|
||||
// Make nice ripple effect on tab
|
||||
|
||||
//noinspection ConstantConditions
|
||||
tabLayout.getTabAt(0).select();
|
||||
final View poorTabView = ((ViewGroup) tabLayout.getChildAt(0)).getChildAt(0);
|
||||
poorTabView.setPressed(true);
|
||||
tabLayout.postDelayed(() -> poorTabView.setPressed(false), 300);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString("accountId", accountId);
|
||||
outState.putSerializable("followState", followState);
|
||||
outState.putBoolean("blocking", blocking);
|
||||
outState.putBoolean("muting", muting);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void obtainAccount() {
|
||||
mastodonApi.account(accountId).enqueue(new Callback<Account>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Account> call,
|
||||
@NonNull Response<Account> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onObtainAccountSuccess(response.body());
|
||||
} else {
|
||||
onObtainAccountFailure();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Account> call, @NonNull Throwable t) {
|
||||
onObtainAccountFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onObtainAccountSuccess(Account account) {
|
||||
loadedAccount = account;
|
||||
|
||||
TextView username = findViewById(R.id.account_username);
|
||||
TextView displayName = findViewById(R.id.account_display_name);
|
||||
TextView note = findViewById(R.id.account_note);
|
||||
|
||||
String usernameFormatted = String.format(
|
||||
getString(R.string.status_username_format), account.getUsername());
|
||||
username.setText(usernameFormatted);
|
||||
|
||||
displayName.setText(account.getName());
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(EmojiCompat.get().process(account.getName()));
|
||||
|
||||
String subtitle = String.format(getString(R.string.status_username_format),
|
||||
account.getUsername());
|
||||
getSupportActionBar().setSubtitle(subtitle);
|
||||
}
|
||||
|
||||
LinkHelper.setClickableText(note, account.getNote(), null, new LinkListener() {
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class);
|
||||
intent.putExtra("hashtag", tag);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(AccountActivity.this, AccountActivity.class);
|
||||
intent.putExtra("id", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewUrl(String url) {
|
||||
viewUrl(url);
|
||||
}
|
||||
});
|
||||
|
||||
if (account.getLocked()) {
|
||||
accountLockedView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
accountLockedView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
Picasso.with(this)
|
||||
.load(account.getAvatar())
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatar);
|
||||
Picasso.with(this)
|
||||
.load(account.getHeader())
|
||||
.placeholder(R.drawable.account_header_default)
|
||||
.into(header);
|
||||
|
||||
NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||
|
||||
String followersCount = numberFormat.format(account.getFollowersCount());
|
||||
String followingCount = numberFormat.format(account.getFollowingCount());
|
||||
String statusesCount = numberFormat.format(account.getStatusesCount());
|
||||
followersTextView.setText(getString(R.string.title_x_followers, followersCount));
|
||||
followingTextView.setText(getString(R.string.title_x_following, followingCount));
|
||||
statusesTextView.setText(getString(R.string.title_x_statuses, statusesCount));
|
||||
|
||||
}
|
||||
|
||||
private void onObtainAccountFailure() {
|
||||
Snackbar.make(tabLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, v -> obtainAccount())
|
||||
.show();
|
||||
}
|
||||
|
||||
private void obtainRelationships() {
|
||||
List<String> ids = new ArrayList<>(1);
|
||||
ids.add(accountId);
|
||||
mastodonApi.relationships(ids).enqueue(new Callback<List<Relationship>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<Relationship>> call,
|
||||
@NonNull Response<List<Relationship>> response) {
|
||||
List<Relationship> relationships = response.body();
|
||||
if (response.isSuccessful() && relationships != null) {
|
||||
Relationship relationship = relationships.get(0);
|
||||
onObtainRelationshipsSuccess(relationship);
|
||||
} else {
|
||||
onObtainRelationshipsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Relationship>> call, @NonNull Throwable t) {
|
||||
onObtainRelationshipsFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onObtainRelationshipsSuccess(Relationship relation) {
|
||||
if (relation.getFollowing()) {
|
||||
followState = FollowState.FOLLOWING;
|
||||
} else if (relation.getRequested()) {
|
||||
followState = FollowState.REQUESTED;
|
||||
} else {
|
||||
followState = FollowState.NOT_FOLLOWING;
|
||||
}
|
||||
this.blocking = relation.getBlocking();
|
||||
this.muting = relation.getMuting();
|
||||
|
||||
if (relation.getFollowedBy()) {
|
||||
followsYouView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
followsYouView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private void updateFollowButton(Button button) {
|
||||
switch (followState) {
|
||||
case NOT_FOLLOWING: {
|
||||
button.setText(R.string.action_follow);
|
||||
break;
|
||||
}
|
||||
case REQUESTED: {
|
||||
button.setText(R.string.state_follow_requested);
|
||||
break;
|
||||
}
|
||||
case FOLLOWING: {
|
||||
button.setText(R.string.action_unfollow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtons() {
|
||||
invalidateOptionsMenu();
|
||||
|
||||
if (!isSelf && !blocking) {
|
||||
floatingBtn.show();
|
||||
followBtn.setVisibility(View.VISIBLE);
|
||||
|
||||
updateFollowButton(followBtn);
|
||||
|
||||
floatingBtn.setOnClickListener(v -> mention());
|
||||
|
||||
followBtn.setOnClickListener(v -> {
|
||||
switch (followState) {
|
||||
case NOT_FOLLOWING: {
|
||||
follow(accountId);
|
||||
break;
|
||||
}
|
||||
case REQUESTED: {
|
||||
showFollowRequestPendingDialog();
|
||||
break;
|
||||
}
|
||||
case FOLLOWING: {
|
||||
showUnfollowWarningDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateFollowButton(followBtn);
|
||||
});
|
||||
} else {
|
||||
floatingBtn.hide();
|
||||
followBtn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onObtainRelationshipsFailure(Exception exception) {
|
||||
Log.e(TAG, "Could not obtain relationships. " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.account_toolbar, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
private String getFollowAction() {
|
||||
switch (followState) {
|
||||
default:
|
||||
case NOT_FOLLOWING:
|
||||
return getString(R.string.action_follow);
|
||||
case REQUESTED:
|
||||
case FOLLOWING:
|
||||
return getString(R.string.action_unfollow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (!isSelf) {
|
||||
MenuItem follow = menu.findItem(R.id.action_follow);
|
||||
follow.setTitle(getFollowAction());
|
||||
follow.setVisible(followState != FollowState.REQUESTED);
|
||||
|
||||
MenuItem block = menu.findItem(R.id.action_block);
|
||||
String title;
|
||||
if (blocking) {
|
||||
title = getString(R.string.action_unblock);
|
||||
} else {
|
||||
title = getString(R.string.action_block);
|
||||
}
|
||||
block.setTitle(title);
|
||||
MenuItem mute = menu.findItem(R.id.action_mute);
|
||||
if (muting) {
|
||||
title = getString(R.string.action_unmute);
|
||||
} else {
|
||||
title = getString(R.string.action_mute);
|
||||
}
|
||||
mute.setTitle(title);
|
||||
} else {
|
||||
// It shouldn't be possible to block or follow yourself.
|
||||
menu.removeItem(R.id.action_follow);
|
||||
menu.removeItem(R.id.action_block);
|
||||
menu.removeItem(R.id.action_mute);
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
private void follow(final String id) {
|
||||
Callback<Relationship> cb = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Relationship> call,
|
||||
@NonNull Response<Relationship> response) {
|
||||
Relationship relationship = response.body();
|
||||
if (response.isSuccessful() && relationship != null) {
|
||||
if (relationship.getFollowing()) {
|
||||
followState = FollowState.FOLLOWING;
|
||||
} else if (relationship.getRequested()) {
|
||||
followState = FollowState.REQUESTED;
|
||||
Snackbar.make(container, R.string.state_follow_requested,
|
||||
Snackbar.LENGTH_LONG).show();
|
||||
} else {
|
||||
followState = FollowState.NOT_FOLLOWING;
|
||||
appstore.dispatch(new UnfollowEvent(id));
|
||||
}
|
||||
updateButtons();
|
||||
} else {
|
||||
onFollowFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
|
||||
onFollowFailure(id);
|
||||
}
|
||||
};
|
||||
|
||||
Assert.expect(followState != FollowState.REQUESTED);
|
||||
switch (followState) {
|
||||
case NOT_FOLLOWING: {
|
||||
mastodonApi.followAccount(id).enqueue(cb);
|
||||
break;
|
||||
}
|
||||
case FOLLOWING: {
|
||||
mastodonApi.unfollowAccount(id).enqueue(cb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onFollowFailure(final String id) {
|
||||
View.OnClickListener listener = v -> follow(id);
|
||||
Snackbar.make(container, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, listener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showFollowRequestPendingDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_message_follow_request)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showUnfollowWarningDialog() {
|
||||
DialogInterface.OnClickListener unfollowListener = (dialogInterface, i) -> follow(accountId);
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_unfollow_warning)
|
||||
.setPositiveButton(android.R.string.ok, unfollowListener)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void block(final String id) {
|
||||
Callback<Relationship> cb = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Relationship> call,
|
||||
@NonNull Response<Relationship> response) {
|
||||
Relationship relationship = response.body();
|
||||
if (response.isSuccessful() && relationship != null) {
|
||||
appstore.dispatch(new BlockEvent(id));
|
||||
blocking = relationship.getBlocking();
|
||||
updateButtons();
|
||||
} else {
|
||||
onBlockFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
|
||||
onBlockFailure(id);
|
||||
}
|
||||
};
|
||||
if (blocking) {
|
||||
mastodonApi.unblockAccount(id).enqueue(cb);
|
||||
} else {
|
||||
mastodonApi.blockAccount(id).enqueue(cb);
|
||||
}
|
||||
}
|
||||
|
||||
private void onBlockFailure(final String id) {
|
||||
View.OnClickListener listener = v -> block(id);
|
||||
Snackbar.make(container, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, listener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void mute(final String id) {
|
||||
Callback<Relationship> cb = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Relationship> call,
|
||||
@NonNull Response<Relationship> response) {
|
||||
Relationship relationship = response.body();
|
||||
if (response.isSuccessful() && relationship != null) {
|
||||
appstore.dispatch(new MuteEvent(id));
|
||||
muting = relationship.getMuting();
|
||||
updateButtons();
|
||||
} else {
|
||||
onMuteFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
|
||||
onMuteFailure(id);
|
||||
}
|
||||
};
|
||||
|
||||
if (muting) {
|
||||
mastodonApi.unmuteAccount(id).enqueue(cb);
|
||||
} else {
|
||||
mastodonApi.muteAccount(id).enqueue(cb);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMuteFailure(final String id) {
|
||||
View.OnClickListener listener = v -> mute(id);
|
||||
Snackbar.make(container, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, listener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean mention() {
|
||||
if (loadedAccount == null) {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
return false;
|
||||
}
|
||||
Intent intent = new ComposeActivity.IntentBuilder()
|
||||
.mentionedUsernames(Collections.singleton(loadedAccount.getUsername()))
|
||||
.build(this);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void broadcast(String action, String id) {
|
||||
Intent intent = new Intent(action);
|
||||
intent.putExtra("id", id);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_mention: {
|
||||
return mention();
|
||||
}
|
||||
case R.id.action_open_in_web: {
|
||||
if (loadedAccount == null) {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
return false;
|
||||
}
|
||||
LinkHelper.openLink(loadedAccount.getUrl(), this);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_follow: {
|
||||
follow(accountId);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_block: {
|
||||
block(accountId);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_mute: {
|
||||
mute(accountId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public FloatingActionButton getActionButton() {
|
||||
if (!isSelf && !blocking) {
|
||||
return floatingBtn;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return dispatchingAndroidInjector;
|
||||
}
|
||||
|
||||
}
|
620
app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt
Normal file
620
app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt
Normal file
|
@ -0,0 +1,620 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.AttrRes
|
||||
import android.support.annotation.ColorInt
|
||||
import android.support.annotation.Px
|
||||
import android.support.design.widget.*
|
||||
import android.support.text.emoji.EmojiCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.widget.TextViewCompat
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.view.RoundedTransformation
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import com.squareup.picasso.Picasso
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import kotlinx.android.synthetic.main.activity_account.*
|
||||
import kotlinx.android.synthetic.main.view_account_moved.*
|
||||
import java.text.NumberFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector, LinkListener {
|
||||
|
||||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: AccountViewModel
|
||||
|
||||
private val accountFieldAdapter = AccountFieldAdapter(this)
|
||||
|
||||
private lateinit var accountId: String
|
||||
private var followState: FollowState? = null
|
||||
private var blocking: Boolean = false
|
||||
private var muting: Boolean = false
|
||||
private var showingReblogs: Boolean = false
|
||||
private var isSelf: Boolean = false
|
||||
private var loadedAccount: Account? = null
|
||||
|
||||
// fields for scroll animation
|
||||
private var hideFab: Boolean = false
|
||||
private var oldOffset: Int = 0
|
||||
@ColorInt
|
||||
private var toolbarColor: Int = 0
|
||||
@ColorInt
|
||||
private var backgroundColor: Int = 0
|
||||
@ColorInt
|
||||
private var statusBarColorTransparent: Int = 0
|
||||
@ColorInt
|
||||
private var statusBarColorOpaque: Int = 0
|
||||
@ColorInt
|
||||
private var textColorPrimary: Int = 0
|
||||
@ColorInt
|
||||
private var textColorSecondary: Int = 0
|
||||
@Px
|
||||
private var avatarSize: Float = 0f
|
||||
@Px
|
||||
private var titleVisibleHeight: Int = 0
|
||||
|
||||
private enum class FollowState {
|
||||
NOT_FOLLOWING,
|
||||
FOLLOWING,
|
||||
REQUESTED
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
|
||||
|
||||
viewModel.accountData.observe(this, Observer<Resource<Account>> {
|
||||
when (it) {
|
||||
is Success -> onAccountChanged(it.data)
|
||||
is Error -> {
|
||||
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { reload() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
|
||||
val relation = it?.data
|
||||
if (relation != null) {
|
||||
onRelationshipChanged(relation)
|
||||
}
|
||||
|
||||
if (it is Error) {
|
||||
Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { reload() }
|
||||
.show()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
val decorView = window.decorView
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_account)
|
||||
|
||||
val intent = intent
|
||||
accountId = intent.getStringExtra(KEY_ACCOUNT_ID)
|
||||
followState = FollowState.NOT_FOLLOWING
|
||||
blocking = false
|
||||
muting = false
|
||||
|
||||
loadedAccount = null
|
||||
|
||||
// set toolbar top margin according to system window insets
|
||||
ViewCompat.setOnApplyWindowInsetsListener(accountCoordinatorLayout) { _, insets ->
|
||||
val top = insets.systemWindowInsetTop
|
||||
|
||||
val toolbarParams = accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams
|
||||
toolbarParams.topMargin = top
|
||||
|
||||
insets.consumeSystemWindowInsets()
|
||||
}
|
||||
|
||||
// Setup the toolbar.
|
||||
setSupportActionBar(accountToolbar)
|
||||
supportActionBar?.title = null
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
hideFab = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fabHide", false)
|
||||
|
||||
toolbarColor = ThemeUtils.getColor(this, R.attr.toolbar_background_color)
|
||||
backgroundColor = ThemeUtils.getColor(this, android.R.attr.colorBackground)
|
||||
statusBarColorTransparent = ContextCompat.getColor(this, R.color.header_background_filter)
|
||||
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
|
||||
textColorPrimary = ThemeUtils.getColor(this, android.R.attr.textColorPrimary)
|
||||
textColorSecondary = ThemeUtils.getColor(this, android.R.attr.textColorSecondary)
|
||||
avatarSize = resources.getDimensionPixelSize(R.dimen.account_activity_avatar_size).toFloat()
|
||||
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
|
||||
|
||||
ThemeUtils.setDrawableTint(this, accountToolbar.navigationIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
|
||||
ThemeUtils.setDrawableTint(this, accountToolbar.overflowIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
|
||||
|
||||
// Add a listener to change the toolbar icon color when it enters/exits its collapsed state.
|
||||
accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
|
||||
@AttrRes var priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed
|
||||
|
||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||
|
||||
@AttrRes val attribute = if (titleVisibleHeight + verticalOffset < 0) {
|
||||
accountToolbar.setTitleTextColor(textColorPrimary)
|
||||
accountToolbar.setSubtitleTextColor(textColorSecondary)
|
||||
|
||||
R.attr.account_toolbar_icon_tint_collapsed
|
||||
} else {
|
||||
accountToolbar.setTitleTextColor(Color.TRANSPARENT)
|
||||
accountToolbar.setSubtitleTextColor(Color.TRANSPARENT)
|
||||
|
||||
R.attr.account_toolbar_icon_tint_uncollapsed
|
||||
}
|
||||
if (attribute != priorAttribute) {
|
||||
priorAttribute = attribute
|
||||
val context = accountToolbar.context
|
||||
ThemeUtils.setDrawableTint(context, accountToolbar.navigationIcon, attribute)
|
||||
ThemeUtils.setDrawableTint(context, accountToolbar.overflowIcon, attribute)
|
||||
}
|
||||
|
||||
if (hideFab && !isSelf && !blocking) {
|
||||
if (verticalOffset > oldOffset) {
|
||||
accountFloatingActionButton.show()
|
||||
}
|
||||
if (verticalOffset < oldOffset) {
|
||||
accountFloatingActionButton.hide()
|
||||
}
|
||||
}
|
||||
oldOffset = verticalOffset
|
||||
|
||||
val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize
|
||||
|
||||
accountAvatarImageView.scaleX = scaledAvatarSize
|
||||
accountAvatarImageView.scaleY = scaledAvatarSize
|
||||
|
||||
accountAvatarImageView.visible(scaledAvatarSize > 0)
|
||||
|
||||
var transparencyPercent = Math.abs(verticalOffset) / titleVisibleHeight.toFloat()
|
||||
if (transparencyPercent > 1) transparencyPercent = 1f
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
|
||||
}
|
||||
|
||||
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
|
||||
val evaluatedTabBarColor = argbEvaluator.evaluate(transparencyPercent, backgroundColor, toolbarColor) as Int
|
||||
accountToolbar.setBackgroundColor(evaluatedToolbarColor)
|
||||
accountHeaderInfoContainer.setBackgroundColor(evaluatedTabBarColor)
|
||||
accountTabLayout.setBackgroundColor(evaluatedTabBarColor)
|
||||
}
|
||||
})
|
||||
|
||||
// Initialise the default UI states.
|
||||
accountFloatingActionButton.hide()
|
||||
accountFollowButton.hide()
|
||||
accountFollowsYouTextView.hide()
|
||||
|
||||
// Obtain information to fill out the profile.
|
||||
viewModel.obtainAccount(accountId)
|
||||
|
||||
val activeAccount = accountManager.activeAccount
|
||||
|
||||
if (accountId == activeAccount?.accountId) {
|
||||
isSelf = true
|
||||
updateButtons()
|
||||
} else {
|
||||
isSelf = false
|
||||
viewModel.obtainRelationship(accountId)
|
||||
}
|
||||
|
||||
// setup the RecyclerView for the account fields
|
||||
accountFieldList.isNestedScrollingEnabled = false
|
||||
accountFieldList.layoutManager = LinearLayoutManager(this)
|
||||
accountFieldList.adapter = accountFieldAdapter
|
||||
|
||||
// Setup the tabs and timeline pager.
|
||||
val adapter = AccountPagerAdapter(supportFragmentManager, accountId)
|
||||
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_media))
|
||||
adapter.setPageTitles(pageTitles)
|
||||
accountFragmentViewPager.pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
|
||||
val pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||
R.drawable.tab_page_margin_dark)
|
||||
accountFragmentViewPager.setPageMarginDrawable(pageMarginDrawable)
|
||||
accountFragmentViewPager.adapter = adapter
|
||||
accountFragmentViewPager.offscreenPageLimit = 2
|
||||
accountTabLayout.setupWithViewPager(accountFragmentViewPager)
|
||||
|
||||
val accountListClickListener = { v: View ->
|
||||
val type = when (v.id) {
|
||||
R.id.accountFollowers-> AccountListActivity.Type.FOLLOWERS
|
||||
R.id.accountFollowing -> AccountListActivity.Type.FOLLOWING
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
val accountListIntent = AccountListActivity.newIntent(this, type, accountId)
|
||||
startActivity(accountListIntent)
|
||||
}
|
||||
accountFollowers.setOnClickListener(accountListClickListener)
|
||||
accountFollowing.setOnClickListener(accountListClickListener)
|
||||
|
||||
accountStatuses.setOnClickListener {
|
||||
// Make nice ripple effect on tab
|
||||
accountTabLayout.getTabAt(0)!!.select()
|
||||
val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0)
|
||||
poorTabView.isPressed = true
|
||||
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccountChanged(account: Account?) {
|
||||
if (account != null) {
|
||||
loadedAccount = account
|
||||
val usernameFormatted = getString(R.string.status_username_format, account.username)
|
||||
accountUsernameTextView.text = usernameFormatted
|
||||
accountDisplayNameTextView.text = CustomEmojiHelper.emojifyString(account.name, account.emojis, accountDisplayNameTextView)
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar?.title = EmojiCompat.get().process(account.name)
|
||||
|
||||
val subtitle = String.format(getString(R.string.status_username_format),
|
||||
account.username)
|
||||
supportActionBar?.subtitle = subtitle
|
||||
}
|
||||
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
|
||||
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
|
||||
|
||||
accountLockedImageView.visible(account.locked)
|
||||
accountBadgeTextView.visible(account.bot)
|
||||
|
||||
Picasso.with(this)
|
||||
.load(account.avatar)
|
||||
.transform(RoundedTransformation(25f))
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(accountAvatarImageView)
|
||||
Picasso.with(this)
|
||||
.load(account.header)
|
||||
.into(accountHeaderImageView)
|
||||
|
||||
accountFieldAdapter.fields = account.fields
|
||||
accountFieldAdapter.emojis = account.emojis
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
if (account.moved != null) {
|
||||
val movedAccount = account.moved
|
||||
|
||||
accountMovedView.show()
|
||||
|
||||
// necessary because accountMovedView is now replaced in layout hierachy
|
||||
findViewById<View>(R.id.accountMovedView).setOnClickListener {
|
||||
onViewAccount(movedAccount.id)
|
||||
}
|
||||
|
||||
accountMovedDisplayName.text = movedAccount.name
|
||||
accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
|
||||
|
||||
Picasso.with(this)
|
||||
.load(movedAccount.avatar)
|
||||
.transform(RoundedTransformation(25f))
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(accountMovedAvatar)
|
||||
|
||||
accountMovedText.text = getString(R.string.account_moved_description, movedAccount.displayName)
|
||||
|
||||
// this is necessary because API 19 can't handle vector compound drawables
|
||||
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
||||
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
movedIcon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(accountMovedText, movedIcon, null, null, null)
|
||||
|
||||
accountFollowers.hide()
|
||||
accountFollowing.hide()
|
||||
accountStatuses.hide()
|
||||
accountTabLayout.hide()
|
||||
accountFragmentViewPager.hide()
|
||||
accountTabBottomShadow.hide()
|
||||
}
|
||||
|
||||
val numberFormat = NumberFormat.getNumberInstance()
|
||||
accountFollowersTextView.text = numberFormat.format(account.followersCount)
|
||||
accountFollowingTextView.text = numberFormat.format(account.followingCount)
|
||||
accountStatusesTextView.text = numberFormat.format(account.statusesCount)
|
||||
|
||||
accountFloatingActionButton.setOnClickListener { _ -> mention() }
|
||||
|
||||
accountFollowButton.setOnClickListener { _ ->
|
||||
if (isSelf) {
|
||||
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
||||
startActivityForResult(intent, EDIT_ACCOUNT)
|
||||
return@setOnClickListener
|
||||
}
|
||||
when (followState) {
|
||||
AccountActivity.FollowState.NOT_FOLLOWING -> {
|
||||
viewModel.changeFollowState(accountId)
|
||||
}
|
||||
AccountActivity.FollowState.REQUESTED -> {
|
||||
showFollowRequestPendingDialog()
|
||||
}
|
||||
AccountActivity.FollowState.FOLLOWING -> {
|
||||
showUnfollowWarningDialog()
|
||||
}
|
||||
}
|
||||
updateFollowButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
//reload account when returning from EditProfileActivity
|
||||
if(requestCode == EDIT_ACCOUNT && resultCode == Activity.RESULT_OK) {
|
||||
viewModel.obtainAccount(accountId, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putString(KEY_ACCOUNT_ID, accountId)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun onRelationshipChanged(relation: Relationship) {
|
||||
followState = when {
|
||||
relation.following -> FollowState.FOLLOWING
|
||||
relation.requested -> FollowState.REQUESTED
|
||||
else -> FollowState.NOT_FOLLOWING
|
||||
}
|
||||
blocking = relation.blocking
|
||||
muting = relation.muting
|
||||
showingReblogs = relation.showingReblogs
|
||||
|
||||
accountFollowsYouTextView.visible(relation.followedBy)
|
||||
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
viewModel.obtainAccount(accountId, true)
|
||||
viewModel.obtainRelationship(accountId)
|
||||
}
|
||||
|
||||
private fun updateFollowButton() {
|
||||
if(isSelf) {
|
||||
accountFollowButton.setText(R.string.action_edit_own_profile)
|
||||
return
|
||||
}
|
||||
when (followState) {
|
||||
AccountActivity.FollowState.NOT_FOLLOWING -> {
|
||||
accountFollowButton.setText(R.string.action_follow)
|
||||
}
|
||||
AccountActivity.FollowState.REQUESTED -> {
|
||||
accountFollowButton.setText(R.string.state_follow_requested)
|
||||
}
|
||||
AccountActivity.FollowState.FOLLOWING -> {
|
||||
accountFollowButton.setText(R.string.action_unfollow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButtons() {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
if (!blocking && loadedAccount?.moved == null) {
|
||||
|
||||
accountFollowButton.show()
|
||||
updateFollowButton()
|
||||
|
||||
if(isSelf) {
|
||||
accountFloatingActionButton.hide()
|
||||
} else {
|
||||
accountFloatingActionButton.show()
|
||||
}
|
||||
|
||||
} else {
|
||||
accountFloatingActionButton.hide()
|
||||
accountFollowButton.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.account_toolbar, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
if (!isSelf) {
|
||||
val follow = menu.findItem(R.id.action_follow)
|
||||
follow.title = if (followState == FollowState.NOT_FOLLOWING) {
|
||||
getString(R.string.action_follow)
|
||||
} else {
|
||||
getString(R.string.action_unfollow)
|
||||
}
|
||||
|
||||
follow.isVisible = followState != FollowState.REQUESTED
|
||||
|
||||
val block = menu.findItem(R.id.action_block)
|
||||
block.title = if (blocking) {
|
||||
getString(R.string.action_unblock)
|
||||
} else {
|
||||
getString(R.string.action_block)
|
||||
}
|
||||
|
||||
val mute = menu.findItem(R.id.action_mute)
|
||||
mute.title = if (muting) {
|
||||
getString(R.string.action_unmute)
|
||||
} else {
|
||||
getString(R.string.action_mute)
|
||||
}
|
||||
|
||||
if (followState == FollowState.FOLLOWING) {
|
||||
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
||||
showReblogs.title = if (showingReblogs) {
|
||||
getString(R.string.action_hide_reblogs)
|
||||
} else {
|
||||
getString(R.string.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
// It shouldn't be possible to block or follow yourself.
|
||||
menu.removeItem(R.id.action_follow)
|
||||
menu.removeItem(R.id.action_block)
|
||||
menu.removeItem(R.id.action_mute)
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun showFollowRequestPendingDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_message_cancel_follow_request)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showUnfollowWarningDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_unfollow_warning)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState(accountId) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.mentionedUsernames(setOf(it.username))
|
||||
.build(this)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = Intent(this, AccountActivity::class.java)
|
||||
intent.putExtra("id", id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
viewUrl(url)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.action_mention -> {
|
||||
mention()
|
||||
return true
|
||||
}
|
||||
R.id.action_open_in_web -> {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
if (loadedAccount != null) {
|
||||
LinkHelper.openLink(loadedAccount?.url, this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_follow -> {
|
||||
viewModel.changeFollowState(accountId)
|
||||
return true
|
||||
}
|
||||
R.id.action_block -> {
|
||||
viewModel.changeBlockState(accountId)
|
||||
return true
|
||||
}
|
||||
R.id.action_mute -> {
|
||||
viewModel.changeMuteState(accountId)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_show_reblogs -> {
|
||||
viewModel.changeShowReblogsState(accountId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun getActionButton(): FloatingActionButton? {
|
||||
return if (!isSelf && !blocking) {
|
||||
accountFloatingActionButton
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
|
||||
return dispatchingAndroidInjector
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EDIT_ACCOUNT = 1457
|
||||
|
||||
private const val KEY_ACCOUNT_ID = "id"
|
||||
private val argbEvaluator = ArgbEvaluator()
|
||||
|
||||
@JvmStatic
|
||||
fun getIntent(context: Context, accountId: String): Intent {
|
||||
val intent = Intent(context, AccountActivity::class.java)
|
||||
intent.putExtra(KEY_ACCOUNT_ID, accountId)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -117,8 +117,7 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
open fun viewAccount(id: String) {
|
||||
val intent = Intent(this, AccountActivity::class.java)
|
||||
intent.putExtra("id", id)
|
||||
val intent = AccountActivity.getIntent(this, id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -155,7 +155,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
if (!headerChanged) {
|
||||
Picasso.with(headerPreview.context)
|
||||
.load(me.header)
|
||||
.placeholder(R.drawable.account_header_default)
|
||||
.into(headerPreview)
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +288,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
if (displayName == null && note == null && locked == null && avatar == null && header == null) {
|
||||
/** if nothing has changed, there is no need to make a network request */
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
@ -302,6 +302,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
privatePreferences.edit()
|
||||
.putBoolean("refreshProfileHeader", true)
|
||||
.apply()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
|
|
|
@ -393,8 +393,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
//open profile when active image was clicked
|
||||
if (current && activeAccount != null) {
|
||||
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
|
||||
intent.putExtra("id", activeAccount.getAccountId());
|
||||
Intent intent = AccountActivity.getIntent(this, activeAccount.getAccountId());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
@ -478,7 +477,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
Picasso.with(MainActivity.this)
|
||||
.load(me.getHeader())
|
||||
.placeholder(R.drawable.account_header_default)
|
||||
.into(background);
|
||||
|
||||
accountManager.updateActiveAccount(me);
|
||||
|
|
|
@ -189,7 +189,7 @@ public class ReportActivity extends BaseActivity implements Injectable {
|
|||
onFetchStatusesFailure((Exception) t);
|
||||
}
|
||||
};
|
||||
mastodonApi.accountStatuses(accountId, null, null, null, null)
|
||||
mastodonApi.accountStatuses(accountId, null, null, null, null, null)
|
||||
.enqueue(callback);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Field
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import kotlinx.android.synthetic.main.item_account_field.view.*
|
||||
|
||||
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
|
||||
|
||||
var emojis: List<Emoji> = emptyList()
|
||||
var fields: List<Field> = emptyList()
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return fields.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountFieldAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account_field, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: AccountFieldAdapter.ViewHolder, position: Int) {
|
||||
viewHolder.nameTextView.text = fields[position].name
|
||||
val emojifiedValue = CustomEmojiHelper.emojifyText(fields[position].value, emojis, viewHolder.valueTextView)
|
||||
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener)
|
||||
}
|
||||
|
||||
class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) {
|
||||
val nameTextView: TextView = rootView.accountFieldName
|
||||
val valueTextView: TextView = rootView.accountFieldValue
|
||||
}
|
||||
|
||||
}
|
|
@ -33,7 +33,8 @@ import javax.inject.Singleton
|
|||
AndroidInjectionModule::class,
|
||||
ActivitiesModule::class,
|
||||
ServicesModule::class,
|
||||
BroadcastReceiverModule::class
|
||||
BroadcastReceiverModule::class,
|
||||
ViewModelModule::class
|
||||
])
|
||||
interface AppComponent {
|
||||
@Component.Builder
|
||||
|
|
|
@ -75,7 +75,7 @@ class NetworkModule {
|
|||
.apply {
|
||||
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
|
||||
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// from https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455
|
||||
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.arch.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import dagger.Binds
|
||||
import dagger.MapKey
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Singleton
|
||||
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
|
||||
}
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
|
||||
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
|
||||
@MapKey
|
||||
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
||||
|
||||
@Module
|
||||
abstract class ViewModelModule {
|
||||
|
||||
@Binds
|
||||
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(AccountViewModel::class)
|
||||
internal abstract fun accountViewModel(viewModel: AccountViewModel): ViewModel
|
||||
|
||||
//Add more ViewModels here
|
||||
}
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Spanned
|
||||
|
@ -26,7 +25,6 @@ import kotlinx.android.parcel.Parceler
|
|||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.parcel.WriteWith
|
||||
|
||||
@SuppressLint("ParcelCreator")
|
||||
@Parcelize
|
||||
data class Account(
|
||||
val id: String,
|
||||
|
@ -41,7 +39,11 @@ data class Account(
|
|||
@SerializedName("followers_count") val followersCount: Int,
|
||||
@SerializedName("following_count") val followingCount: Int,
|
||||
@SerializedName("statuses_count") val statusesCount: Int,
|
||||
val source: AccountSource?
|
||||
val source: AccountSource?,
|
||||
val bot: Boolean,
|
||||
val emojis: List<Emoji> = emptyList(),
|
||||
val fields: List<Field> = emptyList(),
|
||||
val moved: Account? = null
|
||||
|
||||
) : Parcelable {
|
||||
|
||||
|
@ -62,20 +64,25 @@ data class Account(
|
|||
return account?.id == this.id
|
||||
}
|
||||
|
||||
object SpannedParceler : Parceler<Spanned> {
|
||||
override fun create(parcel: Parcel) = HtmlUtils.fromHtml(parcel.readString())
|
||||
|
||||
override fun Spanned.write(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(HtmlUtils.toHtml(this))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@SuppressLint("ParcelCreator")
|
||||
data class AccountSource(
|
||||
val privacy: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
val note: String
|
||||
): Parcelable
|
||||
): Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Field (
|
||||
val name:String,
|
||||
val value: @WriteWith<SpannedParceler>() Spanned
|
||||
): Parcelable
|
||||
|
||||
object SpannedParceler : Parceler<Spanned> {
|
||||
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
|
||||
|
||||
override fun Spanned.write(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(HtmlUtils.toHtml(this))
|
||||
}
|
||||
}
|
|
@ -23,5 +23,6 @@ data class Relationship (
|
|||
@SerializedName("followed_by") val followedBy: Boolean,
|
||||
val blocking: Boolean,
|
||||
val muting: Boolean,
|
||||
val requested: Boolean
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean
|
||||
)
|
||||
|
|
|
@ -167,9 +167,11 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||
intent.putExtra("id", id);
|
||||
startActivity(intent);
|
||||
Context context = getContext();
|
||||
if(context != null) {
|
||||
Intent intent = AccountActivity.getIntent(context, id);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -156,10 +156,10 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener
|
||||
currentCall = if (statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
api.accountStatuses(accountId, null, null, null, true)
|
||||
api.accountStatuses(accountId, null, null, null, null, true)
|
||||
} else {
|
||||
fetchingStatus = FetchingStatus.REFRESHING
|
||||
api.accountStatuses(accountId, null, statuses[0].id, null, true)
|
||||
api.accountStatuses(accountId, null, statuses[0].id, null, null, true)
|
||||
}
|
||||
currentCall?.enqueue(callback)
|
||||
|
||||
|
@ -179,7 +179,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
statuses.lastOrNull()?.let { last ->
|
||||
Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)")
|
||||
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
||||
currentCall = api.accountStatuses(accountId, last.id, null, null, true)
|
||||
currentCall = api.accountStatuses(accountId, last.id, null, null, null, true)
|
||||
currentCall?.enqueue(bottomCallback)
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
|
||||
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
currentCall = api.accountStatuses(accountId, null, null, null, true)
|
||||
currentCall = api.accountStatuses(accountId, null, null, null, null, true)
|
||||
currentCall?.enqueue(callback)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -217,8 +217,7 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
|||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = Intent(context, AccountActivity::class.java)
|
||||
intent.putExtra("id", id)
|
||||
val intent = AccountActivity.getIntent(requireContext(), id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ public class TimelineFragment extends SFragment implements
|
|||
PUBLIC_FEDERATED,
|
||||
TAG,
|
||||
USER,
|
||||
USER_WITH_REPLIES,
|
||||
FAVOURITES,
|
||||
LIST
|
||||
}
|
||||
|
@ -200,7 +201,7 @@ public class TimelineFragment extends SFragment implements
|
|||
Bundle savedInstanceState) {
|
||||
Bundle arguments = Objects.requireNonNull(getArguments());
|
||||
kind = Kind.valueOf(arguments.getString(KIND_ARG));
|
||||
if (kind == Kind.TAG || kind == Kind.USER || kind == Kind.LIST) {
|
||||
if (kind == Kind.TAG || kind == Kind.USER || kind == Kind.USER_WITH_REPLIES|| kind == Kind.LIST) {
|
||||
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
|
||||
}
|
||||
|
||||
|
@ -309,14 +310,20 @@ public class TimelineFragment extends SFragment implements
|
|||
removeAllByAccountId(id);
|
||||
}
|
||||
} else if (event instanceof BlockEvent) {
|
||||
String id = ((BlockEvent) event).getAccountId();
|
||||
removeAllByAccountId(id);
|
||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES) {
|
||||
String id = ((BlockEvent) event).getAccountId();
|
||||
removeAllByAccountId(id);
|
||||
}
|
||||
} else if (event instanceof MuteEvent) {
|
||||
String id = ((MuteEvent) event).getAccountId();
|
||||
removeAllByAccountId(id);
|
||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES) {
|
||||
String id = ((MuteEvent) event).getAccountId();
|
||||
removeAllByAccountId(id);
|
||||
}
|
||||
} else if (event instanceof StatusDeletedEvent) {
|
||||
String id = ((StatusDeletedEvent) event).getStatusId();
|
||||
deleteStatusById(id);
|
||||
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES) {
|
||||
String id = ((StatusDeletedEvent) event).getStatusId();
|
||||
deleteStatusById(id);
|
||||
}
|
||||
} else if (event instanceof StatusComposedEvent) {
|
||||
Status status = ((StatusComposedEvent) event).getStatus();
|
||||
handleStatusComposeEvent(status);
|
||||
|
@ -587,7 +594,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
if (kind == Kind.USER && hashtagOrId.equals(id)) {
|
||||
if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && hashtagOrId.equals(id)) {
|
||||
/* If already viewing an account page, then any requests to view that account page
|
||||
* should be ignored. */
|
||||
return;
|
||||
|
@ -724,7 +731,9 @@ public class TimelineFragment extends SFragment implements
|
|||
case TAG:
|
||||
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, LOAD_AT_ONCE);
|
||||
case USER:
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE, null);
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE, true, null);
|
||||
case USER_WITH_REPLIES:
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE, null, null);
|
||||
case FAVOURITES:
|
||||
return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
|
||||
case LIST:
|
||||
|
@ -1024,6 +1033,7 @@ public class TimelineFragment extends SFragment implements
|
|||
case PUBLIC_LOCAL:
|
||||
break;
|
||||
case USER:
|
||||
case USER_WITH_REPLIES:
|
||||
if (status.getAccount().getId().equals(hashtagOrId)) {
|
||||
break;
|
||||
} else {
|
||||
|
|
|
@ -37,6 +37,7 @@ import okhttp3.MultipartBody;
|
|||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
|
@ -60,12 +61,14 @@ public interface MastodonApi {
|
|||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/public")
|
||||
Call<List<Status>> publicTimeline(
|
||||
@Query("local") Boolean local,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/tag/{hashtag}")
|
||||
Call<List<Status>> hashtagTimeline(
|
||||
@Path("hashtag") String hashtag,
|
||||
|
@ -73,6 +76,7 @@ public interface MastodonApi {
|
|||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/list/{listId}")
|
||||
Call<List<Status>> listTimeline(
|
||||
@Path("listId") String listId,
|
||||
|
@ -85,17 +89,21 @@ public interface MastodonApi {
|
|||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
Call<List<Notification>> notificationsWithAuth(
|
||||
@Header("Authorization") String auth, @Header(DOMAIN_HEADER) String domain);
|
||||
|
||||
@POST("api/v1/notifications/clear")
|
||||
Call<ResponseBody> clearNotifications();
|
||||
|
||||
@GET("api/v1/notifications/{id}")
|
||||
Call<Notification> notification(@Path("id") String notificationId);
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/media")
|
||||
Call<Attachment> uploadMedia(@Part MultipartBody.Part file);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/media/{mediaId}")
|
||||
Call<Attachment> updateMedia(@Path("mediaId") String mediaId,
|
||||
|
@ -113,30 +121,39 @@ public interface MastodonApi {
|
|||
@Field("sensitive") Boolean sensitive,
|
||||
@Field("media_ids[]") List<String> mediaIds,
|
||||
@Header("Idempotency-Key") String idempotencyKey);
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
Call<Status> status(@Path("id") String statusId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/context")
|
||||
Call<StatusContext> statusContext(@Path("id") String statusId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/reblogged_by")
|
||||
Call<List<Account>> statusRebloggedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/statuses/{id}/favourited_by")
|
||||
Call<List<Account>> statusFavouritedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@DELETE("api/v1/statuses/{id}")
|
||||
Call<ResponseBody> deleteStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/reblog")
|
||||
Call<Status> reblogStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/unreblog")
|
||||
Call<Status> unreblogStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/favourite")
|
||||
Call<Status> favouriteStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/unfavourite")
|
||||
Call<Status> unfavouriteStatus(@Path("id") String statusId);
|
||||
|
||||
|
@ -166,9 +183,8 @@ public interface MastodonApi {
|
|||
* @param maxId Only statuses with ID less than maxID will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceID will be returned
|
||||
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
|
||||
* @param onlyMedia Should server return only statuses which contain media. Caution! The server
|
||||
* works in a weird way so if any value if present at this field it will be
|
||||
* interpreted as "true". Pass null to return all statuses.
|
||||
* @param excludeReplies only return statuses that are no replies
|
||||
* @param onlyMedia only return statuses that have media attached
|
||||
* @return
|
||||
*/
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
|
@ -177,29 +193,39 @@ public interface MastodonApi {
|
|||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit,
|
||||
@Nullable @Query("exclude_replies") Boolean excludeReplies,
|
||||
@Nullable @Query("only_media") Boolean onlyMedia);
|
||||
|
||||
@GET("api/v1/accounts/{id}/followers")
|
||||
Call<List<Account>> accountFollowers(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/accounts/{id}/following")
|
||||
Call<List<Account>> accountFollowing(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
Call<Relationship> followAccount(@Path("id") String accountId);
|
||||
Call<Relationship> followAccount(@Path("id") String accountId, @Field("reblogs") boolean showReblogs);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
Call<Relationship> unfollowAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
Call<Relationship> blockAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
Call<Relationship> unblockAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
Call<Relationship> muteAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
Call<Relationship> unmuteAccount(@Path("id") String accountId);
|
||||
|
||||
|
@ -229,8 +255,10 @@ public interface MastodonApi {
|
|||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
Call<Relationship> authorizeFollowRequest(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/reject")
|
||||
Call<Relationship> rejectFollowRequest(@Path("id") String accountId);
|
||||
|
||||
|
|
|
@ -42,6 +42,9 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
|
|||
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId);
|
||||
}
|
||||
case 1: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId);
|
||||
}
|
||||
case 2: {
|
||||
return AccountMediaFragment.newInstance(accountId);
|
||||
}
|
||||
default: {
|
||||
|
@ -52,7 +55,7 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
|
|||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
sealed class Resource<T>(open val data: T?)
|
||||
|
||||
class Loading<T> (override val data: T? = null) : Resource<T>(data)
|
||||
|
||||
class Success<T> (override val data: T? = null) : Resource<T>(data)
|
||||
|
||||
class Error<T> (override val data: T? = null, val errorMessage: String? = null): Resource<T>(data)
|
|
@ -0,0 +1,19 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.view.View
|
||||
|
||||
fun View.show() {
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.hide() {
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun View.visible(visible: Boolean) {
|
||||
this.visibility = if(visible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
): ViewModel() {
|
||||
|
||||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||
|
||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
||||
|
||||
|
||||
fun obtainAccount(accountId: String, reload: Boolean = false) {
|
||||
if(accountData.value == null || reload) {
|
||||
|
||||
accountData.postValue(Loading())
|
||||
|
||||
val call = mastodonApi.account(accountId)
|
||||
call.enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>,
|
||||
response: Response<Account>) {
|
||||
if (response.isSuccessful) {
|
||||
accountData.postValue(Success(response.body()))
|
||||
} else {
|
||||
accountData.postValue(Error())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
accountData.postValue(Error())
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
}
|
||||
}
|
||||
|
||||
fun obtainRelationship(accountId: String, reload: Boolean = false) {
|
||||
if(relationshipData.value == null || reload) {
|
||||
|
||||
relationshipData.postValue(Loading())
|
||||
|
||||
val ids = listOf(accountId)
|
||||
val call = mastodonApi.relationships(ids)
|
||||
call.enqueue(object : Callback<List<Relationship>> {
|
||||
override fun onResponse(call: Call<List<Relationship>>,
|
||||
response: Response<List<Relationship>>) {
|
||||
val relationships = response.body()
|
||||
if (response.isSuccessful && relationships != null) {
|
||||
val relationship = relationships[0]
|
||||
relationshipData.postValue(Success(relationship))
|
||||
} else {
|
||||
relationshipData.postValue(Error())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
||||
relationshipData.postValue(Error())
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeFollowState(id: String) {
|
||||
if (relationshipData.value?.data?.following == true) {
|
||||
changeRelationship(RelationShipAction.UNFOLLOW, id)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.FOLLOW, id)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeBlockState(id: String) {
|
||||
if (relationshipData.value?.data?.blocking == true) {
|
||||
changeRelationship(RelationShipAction.UNBLOCK, id)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.BLOCK, id)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeMuteState(id: String) {
|
||||
if (relationshipData.value?.data?.muting == true) {
|
||||
changeRelationship(RelationShipAction.UNMUTE, id)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.MUTE, id)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeShowReblogsState(id: String) {
|
||||
if (relationshipData.value?.data?.showingReblogs == true) {
|
||||
changeRelationship(RelationShipAction.FOLLOW, id, false)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.FOLLOW, id, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeRelationship(relationshipAction: RelationShipAction, id: String, showReblogs: Boolean = true) {
|
||||
val relation = relationshipData.value?.data
|
||||
val account = accountData.value?.data
|
||||
|
||||
if(relation != null && account != null) {
|
||||
// optimistically post new state for faster response
|
||||
|
||||
val newRelation = when(relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> {
|
||||
if (account.locked) {
|
||||
relation.copy(requested = true)
|
||||
} else {
|
||||
relation.copy(following = true)
|
||||
}
|
||||
}
|
||||
RelationShipAction.UNFOLLOW -> relation.copy(following = false)
|
||||
RelationShipAction.BLOCK -> relation.copy(blocking = true)
|
||||
RelationShipAction.UNBLOCK -> relation.copy(blocking = false)
|
||||
RelationShipAction.MUTE -> relation.copy(muting = true)
|
||||
RelationShipAction.UNMUTE -> relation.copy(muting = false)
|
||||
}
|
||||
relationshipData.postValue(Loading(newRelation))
|
||||
}
|
||||
|
||||
val callback = object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>,
|
||||
response: Response<Relationship>) {
|
||||
val relationship = response.body()
|
||||
if (response.isSuccessful && relationship != null) {
|
||||
relationshipData.postValue(Success(relationship))
|
||||
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(id))
|
||||
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(id))
|
||||
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(id))
|
||||
else -> {}
|
||||
}
|
||||
|
||||
} else {
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
}
|
||||
|
||||
val call = when(relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(id, showReblogs)
|
||||
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(id)
|
||||
RelationShipAction.BLOCK -> mastodonApi.blockAccount(id)
|
||||
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(id)
|
||||
RelationShipAction.MUTE -> mastodonApi.muteAccount(id)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(id)
|
||||
}
|
||||
|
||||
call.enqueue(callback)
|
||||
callList.add(call)
|
||||
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
callList.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
enum class RelationShipAction {
|
||||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue