AccountActivity.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /* Copyright 2017 Andrew Dawson
  2. *
  3. * This file is a part of Tusky.
  4. *
  5. * This program is free software; you can redistribute it and/or modify it under the terms of the
  6. * GNU General Public License as published by the Free Software Foundation; either version 3 of the
  7. * License, or (at your option) any later version.
  8. *
  9. * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
  10. * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
  11. * Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along with Tusky; if not,
  14. * see <http://www.gnu.org/licenses>. */
  15. package com.keylesspalace.tusky;
  16. import android.content.Context;
  17. import android.content.Intent;
  18. import android.content.SharedPreferences;
  19. import android.graphics.drawable.Drawable;
  20. import android.net.Uri;
  21. import android.os.Bundle;
  22. import android.support.annotation.AttrRes;
  23. import android.support.annotation.Nullable;
  24. import android.support.design.widget.AppBarLayout;
  25. import android.support.design.widget.CollapsingToolbarLayout;
  26. import android.support.design.widget.FloatingActionButton;
  27. import android.support.design.widget.Snackbar;
  28. import android.support.design.widget.TabLayout;
  29. import android.support.v4.view.ViewCompat;
  30. import android.support.v4.view.ViewPager;
  31. import android.support.v7.app.ActionBar;
  32. import android.support.v7.widget.Toolbar;
  33. import android.text.method.LinkMovementMethod;
  34. import android.view.Menu;
  35. import android.view.MenuItem;
  36. import android.view.View;
  37. import android.widget.ImageView;
  38. import android.widget.TextView;
  39. import com.keylesspalace.tusky.entity.Account;
  40. import com.keylesspalace.tusky.entity.Relationship;
  41. import com.pkmmte.view.CircularImageView;
  42. import com.squareup.picasso.Picasso;
  43. import java.text.NumberFormat;
  44. import java.util.ArrayList;
  45. import java.util.List;
  46. import butterknife.BindView;
  47. import butterknife.ButterKnife;
  48. import retrofit2.Call;
  49. import retrofit2.Callback;
  50. import retrofit2.Response;
  51. public class AccountActivity extends BaseActivity {
  52. private static final String TAG = "AccountActivity"; // logging tag
  53. private String accountId;
  54. private boolean following = false;
  55. private boolean blocking = false;
  56. private boolean muting = false;
  57. private boolean isSelf;
  58. private TabLayout tabLayout;
  59. private Account loadedAccount;
  60. @BindView(R.id.account_locked) ImageView accountLockedView;
  61. @Override
  62. protected void onCreate(@Nullable Bundle savedInstanceState) {
  63. super.onCreate(savedInstanceState);
  64. setContentView(R.layout.activity_account);
  65. ButterKnife.bind(this);
  66. if (savedInstanceState != null) {
  67. accountId = savedInstanceState.getString("accountId");
  68. } else {
  69. Intent intent = getIntent();
  70. accountId = intent.getStringExtra("id");
  71. }
  72. SharedPreferences preferences = getSharedPreferences(
  73. getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
  74. String loggedInAccountId = preferences.getString("loggedInAccountId", null);
  75. final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
  76. setSupportActionBar(toolbar);
  77. ActionBar actionBar = getSupportActionBar();
  78. if (actionBar != null) {
  79. actionBar.setTitle(null);
  80. actionBar.setDisplayHomeAsUpEnabled(true);
  81. actionBar.setDisplayShowHomeEnabled(true);
  82. }
  83. // Add a listener to change the toolbar icon color when it enters/exits its collapsed state.
  84. AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.account_app_bar_layout);
  85. final CollapsingToolbarLayout collapsingToolbar =
  86. (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
  87. appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
  88. @AttrRes int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed;
  89. @Override
  90. public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
  91. @AttrRes int attribute;
  92. if (collapsingToolbar.getHeight() + verticalOffset
  93. < 2 * ViewCompat.getMinimumHeight(collapsingToolbar)) {
  94. attribute = R.attr.account_toolbar_icon_tint_collapsed;
  95. } else {
  96. attribute = R.attr.account_toolbar_icon_tint_uncollapsed;
  97. }
  98. if (attribute != priorAttribute) {
  99. priorAttribute = attribute;
  100. Context context = toolbar.getContext();
  101. ThemeUtils.setDrawableTint(context, toolbar.getNavigationIcon(), attribute);
  102. ThemeUtils.setDrawableTint(context, toolbar.getOverflowIcon(), attribute);
  103. }
  104. }
  105. });
  106. FloatingActionButton floatingBtn = (FloatingActionButton) findViewById(R.id.floating_btn);
  107. floatingBtn.hide();
  108. CircularImageView avatar = (CircularImageView) findViewById(R.id.account_avatar);
  109. ImageView header = (ImageView) findViewById(R.id.account_header);
  110. avatar.setImageResource(R.drawable.avatar_default);
  111. header.setImageResource(R.drawable.account_header_default);
  112. obtainAccount();
  113. if (!accountId.equals(loggedInAccountId)) {
  114. isSelf = false;
  115. obtainRelationships();
  116. } else {
  117. /* Cause the options menu to update and instead show an options menu for when the
  118. * account being shown is their own account. */
  119. isSelf = true;
  120. invalidateOptionsMenu();
  121. }
  122. // Setup the tabs and timeline pager.
  123. AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), this,
  124. accountId);
  125. String[] pageTitles = {
  126. getString(R.string.title_statuses),
  127. getString(R.string.title_follows),
  128. getString(R.string.title_followers)
  129. };
  130. adapter.setPageTitles(pageTitles);
  131. ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
  132. int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
  133. viewPager.setPageMargin(pageMargin);
  134. Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
  135. R.drawable.tab_page_margin_dark);
  136. viewPager.setPageMarginDrawable(pageMarginDrawable);
  137. viewPager.setAdapter(adapter);
  138. tabLayout = (TabLayout) findViewById(R.id.tab_layout);
  139. tabLayout.setupWithViewPager(viewPager);
  140. for (int i = 0; i < tabLayout.getTabCount(); i++) {
  141. TabLayout.Tab tab = tabLayout.getTabAt(i);
  142. if (tab != null) {
  143. tab.setCustomView(adapter.getTabView(i, tabLayout));
  144. }
  145. }
  146. }
  147. private void obtainAccount() {
  148. mastodonAPI.account(accountId).enqueue(new Callback<Account>() {
  149. @Override
  150. public void onResponse(Call<Account> call, retrofit2.Response<Account> response) {
  151. if (response.isSuccessful()) {
  152. onObtainAccountSuccess(response.body());
  153. } else {
  154. onObtainAccountFailure();
  155. }
  156. }
  157. @Override
  158. public void onFailure(Call<Account> call, Throwable t) {
  159. onObtainAccountFailure();
  160. }
  161. });
  162. }
  163. @Override
  164. protected void onSaveInstanceState(Bundle outState) {
  165. outState.putString("accountId", accountId);
  166. super.onSaveInstanceState(outState);
  167. }
  168. private void onObtainAccountSuccess(Account account) {
  169. loadedAccount = account;
  170. TextView username = (TextView) findViewById(R.id.account_username);
  171. TextView displayName = (TextView) findViewById(R.id.account_display_name);
  172. TextView note = (TextView) findViewById(R.id.account_note);
  173. CircularImageView avatar = (CircularImageView) findViewById(R.id.account_avatar);
  174. ImageView header = (ImageView) findViewById(R.id.account_header);
  175. String usernameFormatted = String.format(
  176. getString(R.string.status_username_format), account.username);
  177. username.setText(usernameFormatted);
  178. displayName.setText(account.getDisplayName());
  179. note.setText(account.note);
  180. note.setLinksClickable(true);
  181. note.setMovementMethod(LinkMovementMethod.getInstance());
  182. if (account.locked) {
  183. accountLockedView.setVisibility(View.VISIBLE);
  184. } else {
  185. accountLockedView.setVisibility(View.GONE);
  186. }
  187. Picasso.with(this)
  188. .load(account.avatar)
  189. .placeholder(R.drawable.avatar_default)
  190. .error(R.drawable.avatar_error)
  191. .into(avatar);
  192. Picasso.with(this)
  193. .load(account.header)
  194. .placeholder(R.drawable.account_header_missing)
  195. .into(header);
  196. NumberFormat nf = NumberFormat.getInstance();
  197. // Add counts to the tabs in the TabLayout.
  198. String[] counts = {
  199. nf.format(Integer.parseInt(account.statusesCount)),
  200. nf.format(Integer.parseInt(account.followingCount)),
  201. nf.format(Integer.parseInt(account.followersCount)),
  202. };
  203. for (int i = 0; i < tabLayout.getTabCount(); i++) {
  204. TabLayout.Tab tab = tabLayout.getTabAt(i);
  205. if (tab != null) {
  206. View view = tab.getCustomView();
  207. if (view != null) {
  208. TextView total = (TextView) view.findViewById(R.id.total);
  209. total.setText(counts[i]);
  210. }
  211. }
  212. }
  213. }
  214. private void onObtainAccountFailure() {
  215. Snackbar.make(tabLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
  216. .setAction(R.string.action_retry, new View.OnClickListener() {
  217. @Override
  218. public void onClick(View v) {
  219. obtainAccount();
  220. }
  221. })
  222. .show();
  223. }
  224. private void obtainRelationships() {
  225. List<String> ids = new ArrayList<>(1);
  226. ids.add(accountId);
  227. mastodonAPI.relationships(ids).enqueue(new Callback<List<Relationship>>() {
  228. @Override
  229. public void onResponse(Call<List<Relationship>> call, retrofit2.Response<List<Relationship>> response) {
  230. if (response.isSuccessful()) {
  231. Relationship relationship = response.body().get(0);
  232. onObtainRelationshipsSuccess(relationship.following, relationship.blocking, relationship.muting);
  233. } else {
  234. onObtainRelationshipsFailure(new Exception(response.message()));
  235. }
  236. }
  237. @Override
  238. public void onFailure(Call<List<Relationship>> call, Throwable t) {
  239. onObtainRelationshipsFailure((Exception) t);
  240. }
  241. });
  242. }
  243. private void onObtainRelationshipsSuccess(boolean following, boolean blocking, boolean muting) {
  244. this.following = following;
  245. this.blocking = blocking;
  246. this.muting = muting;
  247. if (!following || !blocking || !muting) {
  248. invalidateOptionsMenu();
  249. }
  250. updateButtons();
  251. }
  252. private void updateButtons() {
  253. invalidateOptionsMenu();
  254. final FloatingActionButton floatingBtn = (FloatingActionButton) findViewById(R.id.floating_btn);
  255. if(!isSelf && !blocking) {
  256. floatingBtn.show();
  257. if (following) {
  258. floatingBtn.setImageResource(R.drawable.ic_person_minus_24px);
  259. } else {
  260. floatingBtn.setImageResource(R.drawable.ic_person_add_24dp);
  261. }
  262. floatingBtn.setOnClickListener(new View.OnClickListener() {
  263. @Override
  264. public void onClick(View v) {
  265. follow(accountId);
  266. if (following) {
  267. floatingBtn.setImageResource(R.drawable.ic_person_minus_24px);
  268. } else {
  269. floatingBtn.setImageResource(R.drawable.ic_person_add_24dp);
  270. }
  271. }
  272. });
  273. }
  274. }
  275. private void onObtainRelationshipsFailure(Exception exception) {
  276. Log.e(TAG, "Could not obtain relationships. " + exception.getMessage());
  277. }
  278. @Override
  279. public boolean onCreateOptionsMenu(Menu menu) {
  280. getMenuInflater().inflate(R.menu.account_toolbar, menu);
  281. return super.onCreateOptionsMenu(menu);
  282. }
  283. @Override
  284. public boolean onPrepareOptionsMenu(Menu menu) {
  285. if (!isSelf) {
  286. MenuItem follow = menu.findItem(R.id.action_follow);
  287. String title;
  288. if (following) {
  289. title = getString(R.string.action_unfollow);
  290. } else {
  291. title = getString(R.string.action_follow);
  292. }
  293. follow.setTitle(title);
  294. MenuItem block = menu.findItem(R.id.action_block);
  295. if (blocking) {
  296. title = getString(R.string.action_unblock);
  297. } else {
  298. title = getString(R.string.action_block);
  299. }
  300. block.setTitle(title);
  301. MenuItem mute = menu.findItem(R.id.action_mute);
  302. if (muting) {
  303. title = getString(R.string.action_unmute);
  304. } else {
  305. title = getString(R.string.action_mute);
  306. }
  307. mute.setTitle(title);
  308. } else {
  309. // It shouldn't be possible to block or follow yourself.
  310. menu.removeItem(R.id.action_follow);
  311. menu.removeItem(R.id.action_block);
  312. menu.removeItem(R.id.action_mute);
  313. }
  314. return super.onPrepareOptionsMenu(menu);
  315. }
  316. private void follow(final String id) {
  317. Callback<Relationship> cb = new Callback<Relationship>() {
  318. @Override
  319. public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
  320. if (response.isSuccessful()) {
  321. following = response.body().following;
  322. // TODO: display message/indicator when "requested" is true (i.e. when the follow is awaiting approval)
  323. updateButtons();
  324. } else {
  325. onFollowFailure(id);
  326. }
  327. }
  328. @Override
  329. public void onFailure(Call<Relationship> call, Throwable t) {
  330. onFollowFailure(id);
  331. }
  332. };
  333. if (following) {
  334. mastodonAPI.unfollowAccount(id).enqueue(cb);
  335. } else {
  336. mastodonAPI.followAccount(id).enqueue(cb);
  337. }
  338. }
  339. private void onFollowFailure(final String id) {
  340. View.OnClickListener listener = new View.OnClickListener() {
  341. @Override
  342. public void onClick(View v) {
  343. follow(id);
  344. }
  345. };
  346. View anyView = findViewById(R.id.activity_account);
  347. Snackbar.make(anyView, R.string.error_generic, Snackbar.LENGTH_LONG)
  348. .setAction(R.string.action_retry, listener)
  349. .show();
  350. }
  351. private void block(final String id) {
  352. Callback<Relationship> cb = new Callback<Relationship>() {
  353. @Override
  354. public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
  355. if (response.isSuccessful()) {
  356. blocking = response.body().blocking;
  357. updateButtons();
  358. } else {
  359. onBlockFailure(id);
  360. }
  361. }
  362. @Override
  363. public void onFailure(Call<Relationship> call, Throwable t) {
  364. onBlockFailure(id);
  365. }
  366. };
  367. if (blocking) {
  368. mastodonAPI.unblockAccount(id).enqueue(cb);
  369. } else {
  370. mastodonAPI.blockAccount(id).enqueue(cb);
  371. }
  372. }
  373. private void onBlockFailure(final String id) {
  374. View.OnClickListener listener = new View.OnClickListener() {
  375. @Override
  376. public void onClick(View v) {
  377. block(id);
  378. }
  379. };
  380. View anyView = findViewById(R.id.activity_account);
  381. Snackbar.make(anyView, R.string.error_generic, Snackbar.LENGTH_LONG)
  382. .setAction(R.string.action_retry, listener)
  383. .show();
  384. }
  385. private void mute(final String id) {
  386. Callback<Relationship> cb = new Callback<Relationship>() {
  387. @Override
  388. public void onResponse(Call<Relationship> call, Response<Relationship> response) {
  389. if (response.isSuccessful()) {
  390. muting = response.body().muting;
  391. updateButtons();
  392. } else {
  393. onMuteFailure(id);
  394. }
  395. }
  396. @Override
  397. public void onFailure(Call<Relationship> call, Throwable t) {
  398. onMuteFailure(id);
  399. }
  400. };
  401. if (muting) {
  402. mastodonAPI.unmuteAccount(id).enqueue(cb);
  403. } else {
  404. mastodonAPI.muteAccount(id).enqueue(cb);
  405. }
  406. }
  407. private void onMuteFailure(final String id) {
  408. View.OnClickListener listener = new View.OnClickListener() {
  409. @Override
  410. public void onClick(View v) {
  411. mute(id);
  412. }
  413. };
  414. View anyView = findViewById(R.id.activity_account);
  415. Snackbar.make(anyView, R.string.error_generic, Snackbar.LENGTH_LONG)
  416. .setAction(R.string.action_retry, listener)
  417. .show();
  418. }
  419. @Override
  420. public boolean onOptionsItemSelected(MenuItem item) {
  421. switch (item.getItemId()) {
  422. case android.R.id.home: {
  423. onBackPressed();
  424. return true;
  425. }
  426. case R.id.action_mention: {
  427. if (loadedAccount == null) {
  428. // If the account isn't loaded yet, eat the input.
  429. return false;
  430. }
  431. Intent intent = new Intent(this, ComposeActivity.class);
  432. intent.putExtra("mentioned_usernames", new String[] { loadedAccount.username });
  433. startActivity(intent);
  434. return true;
  435. }
  436. case R.id.action_open_in_web: {
  437. if (loadedAccount == null) {
  438. // If the account isn't loaded yet, eat the input.
  439. return false;
  440. }
  441. Uri uri = Uri.parse(loadedAccount.url);
  442. Intent intent = new Intent(Intent.ACTION_VIEW, uri);
  443. startActivity(intent);
  444. return true;
  445. }
  446. case R.id.action_follow: {
  447. follow(accountId);
  448. return true;
  449. }
  450. case R.id.action_block: {
  451. block(accountId);
  452. return true;
  453. }
  454. case R.id.action_mute: {
  455. mute(accountId);
  456. return true;
  457. }
  458. }
  459. return super.onOptionsItemSelected(item);
  460. }
  461. }