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:
Konrad Pozniak 2018-06-18 13:26:18 +02:00 committed by GitHub
parent c450af7b0d
commit 63f9d99390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1422 additions and 978 deletions

View File

@ -75,6 +75,8 @@ dependencies {
implementation "com.android.support:support-emoji:$supportLibraryVersion"
implementation "com.android.support:support-emoji-appcompat:$supportLibraryVersion"
implementation "de.c1710:filemojicompat:1.0.5"
// architecture components
implementation 'android.arch.lifecycle:extensions:1.1.1'
//room
implementation 'android.arch.persistence.room:runtime:1.1.0'
kapt 'android.arch.persistence.room:compiler:1.1.0'

View File

@ -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);
}

View File

@ -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;
}
}

View 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
}
}
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -33,7 +33,8 @@ import javax.inject.Singleton
AndroidInjectionModule::class,
ActivitiesModule::class,
ServicesModule::class,
BroadcastReceiverModule::class
BroadcastReceiverModule::class,
ViewModelModule::class
])
interface AppComponent {
@Component.Builder

View File

@ -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()

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_primary_dark_dark" />
<corners android:radius="14dp"/>
<size android:height="100dp" android:width="100dp"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="@color/text_color_tertiary_dark"/>
<corners android:radius="12dp"/>
<padding android:bottom="2dp" android:top="2dp" android:left="8dp" android:right="8dp"/>
</shape>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="700px" android:height="335px" />
<solid android:color="@color/color_background_dark" />
</shape>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient
android:angle="90"
android:startColor="#FF1a1c23"
android:endColor="#001a1c23"
android:type="linear" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_background_light" />
<corners android:radius="14dp"/>
<size android:height="100dp" android:width="100dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#000"
android:pathData="M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z" />
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="@color/text_color_tertiary_light"/>
<corners android:radius="12dp"/>
<padding android:bottom="2dp" android:top="2dp" android:left="8dp" android:right="8dp"/>
</shape>

View File

@ -2,229 +2,295 @@
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_account"
android:id="@+id/accountCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
android:fillViewport="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/account_app_bar_layout"
android:id="@+id/accountAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.Account.AppBarLayout">
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:layout_height="wrap_content"
app:contentScrim="?attr/toolbar_background_color"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?android:attr/colorBackground"
app:titleEnabled="false">
<RelativeLayout
<ImageView
android:id="@+id/accountHeaderImageView"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_alignTop="@+id/account_header_info"
android:background="?attr/account_header_background_color"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#000" />
<android.support.constraint.ConstraintLayout
android:id="@+id/accountHeaderInfoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/account_header_background_color">
android:layout_marginTop="180dp"
android:paddingEnd="16dp"
android:paddingStart="16dp">
<Button
android:id="@+id/accountFollowButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Follow" />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountDisplayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="62dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Tusky Mastodon Client " />
<TextView
android:id="@+id/accountUsernameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountDisplayNameTextView"
tools:text="\@Tusky" />
<ImageView
android:id="@+id/account_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/account_header_info"
android:layout_alignTop="@+id/account_header_info"
android:background="@drawable/account_header_default"
android:contentDescription="@null"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="pin" />
android:id="@+id/accountLockedImageView"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/accountUsernameTextView"
app:layout_constraintStart_toEndOf="@+id/accountUsernameTextView"
app:layout_constraintTop_toTopOf="@+id/accountUsernameTextView"
app:srcCompat="@drawable/reblog_private_light"
tools:visibility="visible" />
<android.support.constraint.ConstraintLayout
android:id="@+id/account_header_info"
<TextView
android:id="@+id/accountFollowsYouTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/follows_you"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
tools:visibility="visible" />
<TextView
android:id="@+id/accountBadgeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:background="@drawable/profile_badge_background"
android:text="@string/profile_badge_bot_text"
android:textSize="?attr/status_text_small"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/accountFollowsYouTextView"
app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView"
app:layout_goneMarginStart="0dp"
tools:visibility="visible" />
<android.support.constraint.Barrier
android:id="@+id/labelBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountNoteTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/labelBarrier"
android:lineSpacingMultiplier="1.1"
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
tools:text="This is a test description. Descriptions can be quite looooong." />
<android.support.v7.widget.RecyclerView
android:id="@+id/accountFieldList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/account_header_gradient"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="?attr/actionBarSize"
app:layout_collapseMode="parallax">
app:layout_constraintTop_toBottomOf="@id/accountNoteTextView"
tools:itemCount="2"
tools:listitem="@layout/item_account_field" />
<com.pkmmte.view.CircularImageView
android:id="@+id/account_avatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginEnd="10dp"
android:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shadow="true" />
<ViewStub
android:id="@+id/accountMovedView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/accountMovedView"
android:layout="@layout/view_account_moved"
app:layout_constraintTop_toBottomOf="@id/accountFieldList" />
<Button
android:id="@+id/follow_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/accountFollowers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
<TextView
android:id="@+id/account_follows_you"
android:id="@+id/accountFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/follows_you"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="@id/follow_btn"
app:layout_constraintTop_toBottomOf="@id/follow_btn" />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/account_display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintTop_toBottomOf="@id/account_avatar"
tools:text="Tusky Mastodon Client" />
<TextView
android:id="@+id/account_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/account_display_name"
tools:text="\@Tusky" />
<ImageView
android:id="@+id/account_locked"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/account_username"
app:layout_constraintLeft_toRightOf="@id/account_username"
app:srcCompat="@drawable/reblog_private_light"
tools:visibility="visible" />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/account_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/account_username"
android:paddingBottom="16dp"
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/account_username"
tools:text="This is a test description" />
<TextView
android:id="@+id/followers_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:background="@android:color/transparent"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/account_note"
tools:text="3000 Followers" />
tools:text="1234" />
<TextView
android:id="@+id/following_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_followers"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/followers_tv"
app:layout_constraintEnd_toStartOf="@id/statuses_btn"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/followers_tv"
app:layout_constraintTop_toTopOf="@id/followers_tv"
tools:text="500 Following" />
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountFollowing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@id/accountStatuses"
app:layout_constraintStart_toEndOf="@id/accountFollowers"
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
<TextView
android:id="@+id/statuses_btn"
android:id="@+id/accountFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/following_tv"
app:layout_constraintTop_toTopOf="@id/followers_tv"
tools:text="3000 Posts" />
tools:text="500" />
</android.support.constraint.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_follows"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/accountStatuses"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/accountFollowing"
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
<TextView
android:id="@+id/accountStatusesTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="sans-serif-medium"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium"
tools:text="3000" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:text="@string/title_statuses"
android:textColor="@color/account_tab_font_color"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
<!-- top margin equal to statusbar size will be set programmatically -->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:id="@+id/accountToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="?attr/toolbar_popup_theme" />
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.design.widget.TabLayout
android:id="@+id/accountTabLayout"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="?android:colorBackground"
app:tabGravity="fill"
app:tabIndicatorHeight="3dp"
app:tabSelectedTextColor="?attr/colorAccent"
app:tabTextAppearance="@style/TuskyTabAppearance" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:id="@+id/accountFragmentViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
app:tabGravity="fill"
app:tabIndicatorHeight="3dp"
app:tabMaxWidth="0dp"
app:tabSelectedTextColor="?attr/colorAccent"
app:tabTextAppearance="@style/TuskyTabAppearance">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.design.widget.TabLayout>
</android.support.v4.view.ViewPager>
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<View
android:id="@+id/tab_bottom_shadow"
android:id="@+id/accountTabBottomShadow"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@drawable/material_drawer_shadow_bottom"
android:visibility="visible"
app:layout_anchor="@id/tab_layout"
app:layout_anchor="@id/accountTabLayout"
app:layout_anchorGravity="bottom" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/floating_btn"
android:id="@+id/accountFloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
@ -232,6 +298,18 @@
android:contentDescription="@string/action_mention"
app:srcCompat="@drawable/ic_create_24dp" />
<include layout="@layout/item_status_bottom_sheet"/>
<include layout="@layout/item_status_bottom_sheet" />
<ImageView
android:id="@+id/accountAvatarImageView"
android:layout_width="@dimen/account_activity_avatar_size"
android:layout_height="@dimen/account_activity_avatar_size"
android:layout_marginStart="16dp"
android:background="@drawable/avatar_background"
android:padding="3dp"
app:layout_anchor="@+id/accountHeaderInfoContainer"
app:layout_anchorGravity="top"
app:layout_scrollFlags="scroll"
app:srcCompat="@drawable/avatar_default" />
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp">
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountFieldName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@+id/accountFieldValue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_max="160dp"
tools:text="Field title " />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountFieldValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:lineSpacingMultiplier="1.1"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/accountFieldName"
tools:text="Field content. This can contain links and custom emojis" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingTop="12dp">
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountMovedText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="6dp"
android:textSize="?attr/status_text_medium"
tools:text="Account has moved" />
<ImageView
android:id="@+id/accountMovedAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/accountMovedText"
tools:src="@drawable/avatar_default" />
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/accountMovedDisplayName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintBottom_toTopOf="@id/accountMovedUsername"
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
app:layout_constraintTop_toTopOf="@id/accountMovedAvatar"
tools:text="Display name" />
<TextView
android:id="@+id/accountMovedUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/accountMovedAvatar"
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
app:layout_constraintTop_toBottomOf="@id/accountMovedDisplayName"
tools:text="\@username" />
</android.support.constraint.ConstraintLayout>

View File

@ -22,4 +22,9 @@
android:title="@string/action_block"
app:showAsAction="never" />
<item android:id="@+id/action_show_reblogs"
android:title="@string/action_hide_reblogs"
app:showAsAction="never" />
</menu>

View File

@ -140,7 +140,6 @@
<string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string>
<string name="dialog_message_uploading_media">جاري الرفع ...</string>
<string name="dialog_download_image">تنزيل</string>
<string name="dialog_message_follow_request">طلب المتابعة معلق : في إنتظار الرد</string>
<string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب ؟</string>
<string name="visibility_public">عمومي : ينشر على الخيوط العمومية</string>

View File

@ -132,7 +132,6 @@
<string name="dialog_title_finishing_media_upload">S\'està finalitzant la pujada de materila multimèdia</string>
<string name="dialog_message_uploading_media">S\'està pujant...</string>
<string name="dialog_download_image">Baixa</string>
<string name="dialog_message_follow_request">Follow request pending: awaiting their response</string>
<string name="dialog_unfollow_warning">Vols deixar de seguir aquest compte?</string>
<string name="visibility_public">Pública: és visible a la cronologia pública</string>

View File

@ -177,7 +177,7 @@
<string name="action_view_follow_requests">Folgeanfragen</string>
<string name="state_follow_requested">Folgeanfrage gesendet</string>
<string name="confirmation_unmuted">Stummschaltung aufgehoben</string>
<string name="dialog_message_follow_request">Folgeanfrage gesendet: Warten auf Antwort</string>
<string name="dialog_message_cancel_follow_request">Folgeanfrage zurückziehen?</string>
<string name="title_edit_profile">Dein Profil bearbeiten</string>
<string name="login_connection">Verbinden…</string>
@ -293,5 +293,6 @@
<string name="license_blobmoji">Blobmoji ist lizenziert nach der Apache 2.0-Lizenz</string>
<string name="license_twemoji">Twemoji ist als CC-BY-4.0 lizenziert - https://creativecommons.org/licenses/by/4.0/</string>
<string name="download_failed">Download fehlgeschlagen.</string>
<string name="profile_badge_bot_text">Bot</string>
</resources>

View File

@ -140,7 +140,6 @@
<string name="dialog_title_finishing_media_upload">Terminando de subir multimedia</string>
<string name="dialog_message_uploading_media">Subiendo…</string>
<string name="dialog_download_image">Descargar</string>
<string name="dialog_message_follow_request">Solicitud enviada: esperando respuesta</string>
<string name="dialog_unfollow_warning">¿Dejar de seguir esta cuenta?</string>
<string name="visibility_public">Público: Mostrar en historias públicas</string>

View File

@ -130,7 +130,6 @@
<string name="dialog_title_finishing_media_upload">Mise en ligne des médias…</string>
<string name="dialog_message_uploading_media">Envoi en cours…</string>
<string name="dialog_download_image">Télécharger</string>
<string name="dialog_message_follow_request">Demande de suivi en attente de réponse</string>
<string name="visibility_public">Public : afficher dans les fils publics.</string>
<string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics.</string>

View File

@ -130,7 +130,6 @@
<string name="dialog_title_finishing_media_upload">Média feltöltés befejezése</string>
<string name="dialog_message_uploading_media">Feltöltés…</string>
<string name="dialog_download_image">Letöltés</string>
<string name="dialog_message_follow_request">Követési kérés függőben: várjuk a válaszát</string>
<string name="dialog_unfollow_warning">Követés megszüntetése?</string>
<string name="visibility_public">Nyilvános: Nyilvános idővonalra való posztolás</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string>
<string name="dialog_message_uploading_media">アップロード中…</string>
<string name="dialog_download_image">ダウンロード</string>
<string name="dialog_message_follow_request">フォローリクエストの承認待ち:返答を待っています</string>
<string name="dialog_unfollow_warning">このアカウントをフォロー解除しますか?</string>
<string name="visibility_public">公開:公開タイムラインに投稿する</string>

View File

@ -14,9 +14,6 @@
<item name="android:textColorPrimary">@color/text_color_primary_dark</item>
<item name="android:textColorSecondary">@color/text_color_secondary_dark</item>
<item name="android:textColorTertiary">@color/text_color_tertiary_dark</item>
<item name="android:textColorPrimaryInverse">@color/text_color_primary_inverse_dark</item>
<item name="android:textColorSecondaryInverse">@color/text_color_secondary_inverse_dark</item>
<item name="android:textColorTertiaryInverse">@color/text_color_tertiary_inverse_dark</item>
<item name="android:actionMenuTextColor">@color/text_color_primary_dark</item>
<item name="bottomSheetDialogTheme">@style/AppTheme.BottomSheetDialog.Dark</item>
@ -39,7 +36,7 @@
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_dark</item>
<item name="tab_icon_selected_tint">@color/color_accent_dark</item>
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_dark</item>
<item name="account_header_background_color">@color/account_header_background_dark</item>
<item name="account_header_background_color">@color/color_background_dark</item>
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark</item>
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_dark</item>
<item name="toolbar_popup_theme">@style/AppTheme.Account.ToolbarPopupTheme.Dark</item>

View File

@ -135,7 +135,6 @@
<string name="dialog_title_finishing_media_upload">Media upload beëindigen</string>
<string name="dialog_message_uploading_media">Aan het uploaden…</string>
<string name="dialog_download_image">Download</string>
<string name="dialog_message_follow_request">Verzoek van een volger in de wachtrij: wachtend op hun antwoord</string>
<string name="dialog_unfollow_warning">Deze account ontvolgen?</string>
<string name="visibility_public">Publiek: op publieke tijdlijnen posten</string>

View File

@ -144,7 +144,6 @@
<string name="dialog_title_finishing_media_upload">Finalizacion del mandadís del mèdia</string>
<string name="dialog_message_uploading_media">Mandadís...</string>
<string name="dialog_download_image">Telecargar</string>
<string name="dialog_message_follow_request">Demanda dabonament en espèra : responsas esperadas</string>
<string name="dialog_unfollow_warning">Volètz quitar de seguir aqueste compte ?</string>
<string name="visibility_public">Publica : es visibla a la cronologia publica</string>

View File

@ -155,7 +155,6 @@
<string name="dialog_title_finishing_media_upload">Kończenie wysyłania treści</string>
<string name="dialog_message_uploading_media">Wysyłanie…</string>
<string name="dialog_download_image">Pobierz</string>
<string name="dialog_message_follow_request">Oczekująca prośba o możliwość śledzenia: oczekiwanie na odpowiedź</string>
<string name="dialog_unfollow_warning">Czy chcesz przestać śledzić to konto?</string>

View File

@ -133,7 +133,6 @@
<string name="dialog_title_finishing_media_upload">Envio de mídia terminando</string>
<string name="dialog_message_uploading_media">Enviando…</string>
<string name="dialog_download_image">Baixar</string>
<string name="dialog_message_follow_request">Solicitação pendente: esperando por resposta</string>
<string name="dialog_unfollow_warning">Deixar de seguir esta conta?</string>
<string name="visibility_public">Público: Postar em timelines públicas</string>

View File

@ -144,7 +144,6 @@
<string name="dialog_title_finishing_media_upload">Завершается загрузка медиаконтента</string>
<string name="dialog_message_uploading_media">Загружается…</string>
<string name="dialog_download_image">Скачать</string>
<string name="dialog_message_follow_request">Статус запроса на подписку: ожидается ответ</string>
<string name="dialog_unfollow_warning">Отписаться от этого аккаунта?</string>
<string name="visibility_public">Публичный: Показать в публичных лентах</string>

View File

@ -129,7 +129,6 @@
<string name="dialog_title_finishing_media_upload">மீடியா பதிவேற்றம் முடிகிறது</string>
<string name="dialog_message_uploading_media">ஏற்றுகிறது ...</string>
<string name="dialog_download_image">பதிவிறக்க</string>
<string name="dialog_message_follow_request">கோரிக்கை நிலுவையில் உள்ளது, அவர்களின் பதிலுக்காக காத்திருக்கிறது</string>
<string name="dialog_unfollow_warning">இந்த கணக்கை பின்பற்ற வேண்டாமா?</string>
<string name="visibility_public">அனைவருக்கும் காண்பி</string>

View File

@ -115,7 +115,6 @@
<string name="dialog_title_finishing_media_upload">Medya Yükleme Bittiriliyor</string>
<string name="dialog_message_uploading_media">Yükleniyor…</string>
<string name="dialog_download_image">İndir</string>
<string name="dialog_message_follow_request">Takip etme istekleri: yanıt bekleniyor</string>
<string name="visibility_public">Kamu: Herkese açık ve sosyal çizelgelerinde çıkar</string>
<string name="visibility_unlisted">Saklı: Herkese açık ancak sosyal çizelgesinde çıkmaz</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">正在结束上传…</string>
<string name="dialog_message_uploading_media">正在上传…</string>
<string name="dialog_download_image">下载</string>
<string name="dialog_message_follow_request">关注请求已发送,等待对方回复</string>
<string name="dialog_unfollow_warning">不再关注此用户?</string>
<string name="visibility_public">公开:所有人可见,并会出现在公共时间轴上</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">正在結束上傳…</string>
<string name="dialog_message_uploading_media">正在上傳…</string>
<string name="dialog_download_image">下載</string>
<string name="dialog_message_follow_request">關注請求已發送,等待對方回覆</string>
<string name="dialog_unfollow_warning">不再關注此用户?</string>
<string name="visibility_public">公開:所有人可見,並會出現在公共時間軸上</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">正在結束上傳…</string>
<string name="dialog_message_uploading_media">正在上傳…</string>
<string name="dialog_download_image">下載</string>
<string name="dialog_message_follow_request">關注請求已發送,等待對方回覆</string>
<string name="dialog_unfollow_warning">不再關注此用户?</string>
<string name="visibility_public">公開:所有人可見,並會出現在公共時間軸上</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">正在结束上传…</string>
<string name="dialog_message_uploading_media">正在上传…</string>
<string name="dialog_download_image">下载</string>
<string name="dialog_message_follow_request">关注请求已发送,等待对方回复</string>
<string name="dialog_unfollow_warning">不再关注此用户?</string>
<string name="visibility_public">公开:所有人可见,并会出现在公共时间轴上</string>

View File

@ -136,7 +136,6 @@
<string name="dialog_title_finishing_media_upload">正在結束上傳…</string>
<string name="dialog_message_uploading_media">正在上傳…</string>
<string name="dialog_download_image">下載</string>
<string name="dialog_message_follow_request">關注請求已發送,等待對方回覆</string>
<string name="dialog_unfollow_warning">不再關注此用戶?</string>
<string name="visibility_public">公開:所有人可見,並會出現在公共時間軸上</string>

View File

@ -33,7 +33,6 @@
<color name="status_divider_dark">#2f3441</color>
<color name="tab_page_margin_dark">#1a1c23</color>
<color name="account_toolbar_icon_collapsed_dark">#FFFFFF</color>
<color name="account_header_background_dark">#000000</color>
<color name="compose_media_button_disabled_dark">#586173</color>
<color name="compose_mention_dark">#AFBFCF</color>
<color name="report_status_background_dark">#000000</color>
@ -66,7 +65,6 @@
<color name="status_divider_light">#cfcfcf</color>
<color name="tab_page_margin_light">#cfcfcf</color>
<color name="account_toolbar_icon_collapsed_light">#DE000000</color>
<color name="account_header_background_light">#EFEFEF</color>
<color name="compose_media_button_disabled_light">#a3a5ab</color>
<color name="compose_mention_light">#2F5F6F</color>
<color name="report_status_background_light">#EFEFEF</color>

View File

@ -20,8 +20,10 @@
<dimen name="compose_activity_snackbar_elevation">16dp</dimen>
<dimen name="compose_activity_scrollview_height">-1px</dimen> <!-- match_parent -->
<dimen name="account_activity_scroll_title_visible_height">200dp</dimen>
<dimen name="account_activity_avatar_size">100dp</dimen>
<dimen name="compose_activity_scrollview_height">-1px</dimen> <!-- match_parent -->
<dimen name="timeline_width">-1px</dimen> <!-- match_parent -->
</resources>

View File

@ -28,6 +28,7 @@
<string name="title_view_thread">Toot</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Posts</string>
<string name="title_statuses_with_replies">With replies</string>
<string name="title_follows">Follows</string>
<string name="title_followers">Followers</string>
<string name="title_favourites">Favourites</string>
@ -70,6 +71,8 @@
<string name="action_unfollow">Unfollow</string>
<string name="action_block">Block</string>
<string name="action_unblock">Unblock</string>
<string name="action_hide_reblogs">Hide boosts</string>
<string name="action_show_reblogs">Show boosts</string>
<string name="action_report">Report</string>
<string name="action_delete">Delete</string>
<string name="action_send">TOOT</string>
@ -94,6 +97,7 @@
<string name="action_open_drawer">Open drawer</string>
<string name="action_save">Save</string>
<string name="action_edit_profile">Edit profile</string>
<string name="action_edit_own_profile">Edit</string>
<string name="action_undo">Undo</string>
<string name="action_accept">Accept</string>
<string name="action_reject">Reject</string>
@ -146,7 +150,7 @@
<string name="dialog_title_finishing_media_upload">Finishing Media Upload</string>
<string name="dialog_message_uploading_media">Uploading…</string>
<string name="dialog_download_image">Download</string>
<string name="dialog_message_follow_request">Follow request pending: awaiting their response</string>
<string name="dialog_message_cancel_follow_request">Revoke the follow request?</string>
<string name="dialog_unfollow_warning">Unfollow this account?</string>
<string name="visibility_public">Public: Post to public timelines</string>
@ -334,6 +338,10 @@
<string name="license_blobmoji">Blobmoji is licensed under the Apache license 2.0</string>
<string name="license_twemoji">Twemoji is licensed as CC-BY 4.0 - https://creativecommons.org/licenses/by/4.0/</string>
<string name="download_failed">Download failed</string>
<string name="profile_badge_bot_text">Bot</string>
<string name="account_moved_description">%1$s has moved to:</string>
<string name="reblog_private">Boost to original audience</string>
<string name="unreblog_private">Unboost</string>

View File

@ -63,11 +63,7 @@
<item name="android:textColorPrimary">@color/text_color_primary_light</item>
<item name="android:textColorSecondary">@color/text_color_secondary_light</item>
<item name="android:textColorTertiary">@color/text_color_tertiary_light</item>
<item name="android:textColorPrimaryInverse">@color/text_color_primary_inverse_light</item>
<item name="android:textColorSecondaryInverse">@color/text_color_secondary_inverse_light
</item>
<item name="android:textColorTertiaryInverse">@color/text_color_tertiary_inverse_light
</item>
<item name="android:actionMenuTextColor">@color/text_color_primary_light</item>
<item name="bottomSheetDialogTheme">@style/AppTheme.BottomSheetDialog.Light</item>
@ -93,7 +89,7 @@
</item>
<item name="tab_icon_selected_tint">@color/color_accent_light</item>
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_light</item>
<item name="account_header_background_color">@color/account_header_background_light</item>
<item name="account_header_background_color">@color/color_primary_dark_light</item>
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark
</item> <!--Default to dark on purpose, because header backgrounds with gradients are always dark.-->
<item name="account_toolbar_icon_tint_collapsed">

View File

@ -57,7 +57,8 @@ class BottomSheetActivityTest {
0,
0,
0,
null
null,
false
)
private val accountCallback = FakeSearchResults(account)

View File

@ -255,7 +255,8 @@ class ComposeActivityTest {
0,
0,
0,
null
null,
false
),
maximumTootCharacters
)