From 60d68b0ae6a819b85a47e60f314c33b762f1abe1 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Fri, 27 Jan 2017 22:33:43 -0500 Subject: [PATCH] Partial account profile pages now in. Follows/Followers tabs are empty and block/follow does nothing yet. --- app/src/main/AndroidManifest.xml | 1 + .../keylesspalace/tusky/AccountActivity.java | 267 ++++++++++++++++++ .../keylesspalace/tusky/AccountFragment.java | 45 +++ .../tusky/AccountPagerAdapter.java | 62 ++++ .../tusky/AdapterItemRemover.java | 15 + .../com/keylesspalace/tusky/HtmlUtils.java | 42 +++ .../com/keylesspalace/tusky/MainActivity.java | 88 ++++++ .../tusky/NotificationsFragment.java | 11 + .../com/keylesspalace/tusky/SFragment.java | 30 +- .../keylesspalace/tusky/SplashActivity.java | 7 + .../java/com/keylesspalace/tusky/Status.java | 22 +- .../tusky/StatusActionListener.java | 2 + .../keylesspalace/tusky/StatusViewHolder.java | 49 +++- .../keylesspalace/tusky/TimelineFragment.java | 39 ++- .../tusky/ViewThreadFragment.java | 11 + .../res/drawable/account_header_default.png | Bin 0 -> 1891 bytes app/src/main/res/layout/activity_account.xml | 122 ++++++++ app/src/main/res/layout/item_status.xml | 1 + app/src/main/res/menu/account_toolbar.xml | 18 ++ app/src/main/res/menu/main_toolbar.xml | 5 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 18 +- 22 files changed, 791 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java create mode 100644 app/src/main/res/drawable/account_header_default.png create mode 100644 app/src/main/res/layout/activity_account.xml create mode 100644 app/src/main/res/menu/account_toolbar.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3053a174..b16c9ce8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ + . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.TabLayout; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.text.Spanned; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.NetworkImageView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class AccountActivity extends AppCompatActivity { + private String domain; + private String accessToken; + private boolean following = false; + private boolean blocking = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_account); + + Intent intent = getIntent(); + String username = intent.getStringExtra("username"); + String id = intent.getStringExtra("id"); + TextView accountName = (TextView) findViewById(R.id.account_username); + accountName.setText(username); + + SharedPreferences preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + domain = preferences.getString("domain", null); + accessToken = preferences.getString("accessToken", null); + assert(domain != null); + assert(accessToken != null); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + NetworkImageView avatar = (NetworkImageView) findViewById(R.id.account_avatar); + NetworkImageView header = (NetworkImageView) findViewById(R.id.account_header); + avatar.setDefaultImageResId(R.drawable.avatar_default); + avatar.setErrorImageResId(R.drawable.avatar_error); + header.setDefaultImageResId(R.drawable.account_header_default); + + obtainAccount(id); + obtainRelationships(id); + + // Setup the tabs and timeline pager. + AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), id); + String[] pageTitles = { + getString(R.string.title_statuses), + getString(R.string.title_follows), + getString(R.string.title_followers) + }; + adapter.setPageTitles(pageTitles); + ViewPager viewPager = (ViewPager) findViewById(R.id.pager); + viewPager.setAdapter(adapter); + TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout); + tabLayout.setupWithViewPager(viewPager); + } + + private void obtainAccount(String id) { + String endpoint = String.format(getString(R.string.endpoint_accounts), id); + String url = "https://" + domain + endpoint; + JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + onObtainAccountSuccess(response); + } catch (JSONException e) { + onObtainAccountFailure(); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onObtainAccountFailure(); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(this).addToRequestQueue(request); + } + + private void onObtainAccountSuccess(JSONObject response) throws JSONException { + TextView username = (TextView) findViewById(R.id.account_username); + TextView displayName = (TextView) findViewById(R.id.account_display_name); + TextView note = (TextView) findViewById(R.id.account_note); + NetworkImageView avatar = (NetworkImageView) findViewById(R.id.account_avatar); + NetworkImageView header = (NetworkImageView) findViewById(R.id.account_header); + + String usernameFormatted = String.format( + getString(R.string.status_username_format), response.getString("acct")); + username.setText(usernameFormatted); + + String displayNameString = response.getString("display_name"); + displayName.setText(displayNameString); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(displayNameString); + } + + String noteHtml = response.getString("note"); + Spanned noteSpanned = HtmlUtils.fromHtml(noteHtml); + note.setText(noteSpanned); + + ImageLoader imageLoader = VolleySingleton.getInstance(this).getImageLoader(); + avatar.setImageUrl(response.getString("avatar"), imageLoader); + String headerUrl = response.getString("header"); + if (!headerUrl.isEmpty() && !headerUrl.equals("/headers/original/missing.png")) { + header.setImageUrl(headerUrl, imageLoader); + } + } + + private void onObtainAccountFailure() { + //TODO: help + assert(false); + } + + private void obtainRelationships(String id) { + String endpoint = getString(R.string.endpoint_relationships); + String url = String.format("https://%s%s?id=%s", domain, endpoint, id); + JsonArrayRequest request = new JsonArrayRequest(url, + new Response.Listener() { + @Override + public void onResponse(JSONArray response) { + boolean following; + boolean blocking; + try { + JSONObject object = response.getJSONObject(0); + following = object.getBoolean("following"); + blocking = object.getBoolean("blocking"); + } catch (JSONException e) { + onObtainRelationshipsFailure(); + return; + } + onObtainRelationshipsSuccess(following, blocking); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onObtainRelationshipsFailure(); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(this).addToRequestQueue(request); + } + + private void onObtainRelationshipsSuccess(boolean following, boolean blocking) { + this.following = following; + this.blocking = blocking; + if (!following || !blocking) { + invalidateOptionsMenu(); + } + } + + private void onObtainRelationshipsFailure() { + //TODO: help + assert(false); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.account_toolbar, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem follow = menu.findItem(R.id.action_follow); + String title; + if (following) { + title = getString(R.string.action_unfollow); + } else { + title = getString(R.string.action_follow); + } + follow.setTitle(title); + MenuItem block = menu.findItem(R.id.action_block); + if (blocking) { + title = getString(R.string.action_unblock); + } else { + title = getString(R.string.action_block); + } + block.setTitle(title); + return super.onPrepareOptionsMenu(menu); + } + + private void follow() { + + } + + private void block() { + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_back: { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + } + case R.id.action_follow: { + follow(); + return true; + } + case R.id.action_block: { + block(); + return true; + } + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java new file mode 100644 index 00000000..2f8cbbb5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -0,0 +1,45 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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 + * . */ + +package com.keylesspalace.tusky; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +public class AccountFragment extends Fragment { + public enum Type { + FOLLOWS, + FOLLOWERS, + } + + public static AccountFragment newInstance(Type type) { + Bundle arguments = new Bundle(); + AccountFragment fragment = new AccountFragment(); + arguments.putString("type", type.name()); + fragment.setArguments(arguments); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return super.onCreateView(inflater, container, savedInstanceState); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java new file mode 100644 index 00000000..0ec0f1b7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java @@ -0,0 +1,62 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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 + * . */ + +package com.keylesspalace.tusky; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +public class AccountPagerAdapter extends FragmentPagerAdapter { + private String accountId; + private String[] pageTitles; + + public AccountPagerAdapter(FragmentManager manager, String accountId) { + super(manager); + this.accountId = accountId; + } + + public void setPageTitles(String[] titles) { + pageTitles = titles; + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: { + return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId); + } + case 1: { + return AccountFragment.newInstance(AccountFragment.Type.FOLLOWS); + } + case 2: { + return AccountFragment.newInstance(AccountFragment.Type.FOLLOWERS); + } + default: { + return null; + } + } + } + + @Override + public int getCount() { + return 3; + } + + @Override + public CharSequence getPageTitle(int position) { + return pageTitles[position]; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java b/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java index c52a88bb..3791d7c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java +++ b/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java @@ -1,3 +1,18 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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 + * . */ + package com.keylesspalace.tusky; public interface AdapterItemRemover { diff --git a/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java b/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java new file mode 100644 index 00000000..7d62b464 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java @@ -0,0 +1,42 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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 + * . */ + +package com.keylesspalace.tusky; + +import android.os.Build; +import android.text.Html; +import android.text.Spanned; + +public class HtmlUtils { + private static CharSequence trimTrailingWhitespace(CharSequence s) { + int i = s.length(); + do { + i--; + } while (i >= 0 && Character.isWhitespace(s.charAt(i))); + return s.subSequence(0, i + 1); + } + + public static Spanned fromHtml(String html) { + Spanned result; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + result = Html.fromHtml(html); + } + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * all status contents do, so it should be trimmed. */ + return (Spanned) trimTrailingWhitespace(result); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 6af79ddb..83167d0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -29,16 +29,33 @@ import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + public class MainActivity extends AppCompatActivity { private AlarmManager alarmManager; private PendingIntent serviceAlarmIntent; private boolean notificationServiceEnabled; + private String loggedInAccountId; + private String loggedInAccountUsername; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + // Fetch user info while we're doing other things. + fetchUserInfo(); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); @@ -75,11 +92,78 @@ public class MainActivity extends AppCompatActivity { } } + private void fetchUserInfo() { + SharedPreferences preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + String domain = preferences.getString("domain", null); + final String accessToken = preferences.getString("accessToken", null); + String id = preferences.getString("loggedInAccountId", null); + String username = preferences.getString("loggedInAccountUsername", null); + if (id != null && username != null) { + loggedInAccountId = id; + loggedInAccountUsername = username; + } else { + String endpoint = getString(R.string.endpoint_verify_credentials); + String url = "https://" + domain + endpoint; + JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + String id = response.getString("id"); + String username = response.getString("acct"); + onFetchUserInfoSuccess(id, username); + } catch (JSONException e) { + //TODO: Help + assert (false); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onFetchUserInfoFailure(); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(this).addToRequestQueue(request); + } + } + + private void onFetchUserInfoSuccess(String id, String username) { + loggedInAccountId = id; + loggedInAccountUsername = username; + SharedPreferences preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("loggedInAccountId", loggedInAccountId); + editor.putString("loggedInAccountUsername", loggedInAccountUsername); + editor.apply(); + } + + private void onFetchUserInfoFailure() { + //TODO: help + assert(false); + } + private void compose() { Intent intent = new Intent(this, ComposeActivity.class); startActivity(intent); } + private void viewProfile() { + Intent intent = new Intent(this, AccountActivity.class); + intent.putExtra("id", loggedInAccountId); + intent.putExtra("username", loggedInAccountUsername); + startActivity(intent); + } + private void logOut() { if (notificationServiceEnabled) { alarmManager.cancel(serviceAlarmIntent); @@ -108,6 +192,10 @@ public class MainActivity extends AppCompatActivity { compose(); return true; } + case R.id.action_profile: { + viewProfile(); + return true; + } case R.id.action_logout: { logOut(); return true; diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java index 5827a517..e1c59b2a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -202,4 +202,15 @@ public class NotificationsFragment extends SFragment implements public void onViewTag(String tag) { super.viewTag(tag); } + + public void onViewAccount(String id, String username) { + super.viewAccount(id, username); + } + + public void onViewAccount(int position) { + Status status = adapter.getItem(position).getStatus(); + String id = status.getAccountId(); + String username = status.getUsername(); + super.viewAccount(id, username); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index 23a0ac4e..e41ec81a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -33,7 +33,6 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; -import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -61,10 +60,12 @@ public class SFragment extends Fragment { getString(R.string.preferences_file_key), Context.MODE_PRIVATE); domain = preferences.getString("domain", null); accessToken = preferences.getString("accessToken", null); + loggedInAccountId = preferences.getString("loggedInAccountId", null); + loggedInUsername = preferences.getString("loggedInAccountUsername", null); assert(domain != null); assert(accessToken != null); - - sendUserInfoRequest(); + assert(loggedInAccountId != null); + assert(loggedInUsername != null); } protected void sendRequest( @@ -100,22 +101,6 @@ public class SFragment extends Fragment { sendRequest(Request.Method.POST, endpoint, null, null); } - private void sendUserInfoRequest() { - sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - try { - loggedInAccountId = response.getString("id"); - loggedInUsername = response.getString("acct"); - } catch (JSONException e) { - //TODO: Help - assert(false); - } - } - }); - } - protected void reply(Status status) { String inReplyToId = status.getId(); Status.Mention[] mentions = status.getMentions(); @@ -250,4 +235,11 @@ public class SFragment extends Fragment { intent.putExtra("hashtag", tag); startActivity(intent); } + + protected void viewAccount(String id, String username) { + Intent intent = new Intent(getContext(), AccountActivity.class); + intent.putExtra("id", id); + intent.putExtra("username", username); + startActivity(intent); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java index 1297196c..865e8e41 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java @@ -21,6 +21,13 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.toolbox.JsonObjectRequest; + +import org.json.JSONException; +import org.json.JSONObject; + public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/com/keylesspalace/tusky/Status.java b/app/src/main/java/com/keylesspalace/tusky/Status.java index 6c73c1ef..227787ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/Status.java @@ -182,26 +182,6 @@ public class Status { return date; } - private static CharSequence trimTrailingWhitespace(CharSequence s) { - int i = s.length(); - do { - i--; - } while (i >= 0 && Character.isWhitespace(s.charAt(i))); - return s.subSequence(0, i + 1); - } - - private static Spanned compatFromHtml(String html) { - Spanned result; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); - } else { - result = Html.fromHtml(html); - } - /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which - * all status contents do, so it should be trimmed. */ - return (Spanned) trimTrailingWhitespace(result); - } - public static Status parse(JSONObject object, boolean isReblog) throws JSONException { String id = object.getString("id"); String content = object.getString("content"); @@ -264,7 +244,7 @@ public class Status { status = reblog; status.setRebloggedByUsername(username); } else { - Spanned contentPlus = compatFromHtml(content); + Spanned contentPlus = HtmlUtils.fromHtml(content); status = new Status( id, accountId, displayName, username, contentPlus, avatar, createdAt, reblogged, favourited, visibility); diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java index b23b373d..dabdf0e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -25,4 +25,6 @@ public interface StatusActionListener { void onViewMedia(String url, Status.MediaAttachment.Type type); void onViewThread(int position); void onViewTag(String tag); + void onViewAccount(String id, String username); + void onViewAccount(int position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java index 629c57c5..1f891242 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java @@ -98,7 +98,8 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { username.setText(usernameText); } - public void setContent(Spanned content, final StatusActionListener listener) { + public void setContent(Spanned content, Status.Mention[] mentions, + final StatusActionListener listener) { // Redirect URLSpan's in the status content to the listener for viewing tag pages. SpannableStringBuilder builder = new SpannableStringBuilder(content); URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); @@ -106,17 +107,36 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { int start = builder.getSpanStart(span); int end = builder.getSpanEnd(span); int flags = builder.getSpanFlags(span); - CharSequence tag = builder.subSequence(start, end); - if (tag.charAt(0) == '#') { - final String viewTag = tag.subSequence(1, tag.length()).toString(); + CharSequence text = builder.subSequence(start, end); + if (text.charAt(0) == '#') { + final String tag = text.subSequence(1, text.length()).toString(); ClickableSpan newSpan = new ClickableSpan() { @Override public void onClick(View widget) { - listener.onViewTag(viewTag); + listener.onViewTag(tag); } }; builder.removeSpan(span); builder.setSpan(newSpan, start, end, flags); + } else if (text.charAt(0) == '@') { + final String accountUsername = text.subSequence(1, text.length()).toString(); + String id = null; + for (Status.Mention mention: mentions) { + if (mention.getUsername().equals(accountUsername)) { + id = mention.getId(); + } + } + if (id != null) { + final String accountId = id; + ClickableSpan newSpan = new ClickableSpan() { + @Override + public void onClick(View widget) { + listener.onViewAccount(accountId, accountUsername); + } + }; + builder.removeSpan(span); + builder.setSpan(newSpan, start, end, flags); + } } } // Set the contents. @@ -236,7 +256,12 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { } public void setupButtons(final StatusActionListener listener, final int position) { - + avatar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewAccount(position); + } + }); replyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -261,19 +286,25 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { listener.onMore(v, position); } }); - container.setOnClickListener(new View.OnClickListener() { + /* Even though the content TextView is a child of the container, it won't respond to clicks + * if it contains URLSpans without also setting its listener. The surrounding spans will + * just eat the clicks instead of deferring to the parent listener, but WILL respond to a + * listener directly on the TextView, for whatever reason. */ + View.OnClickListener viewThreadListener = new View.OnClickListener() { @Override public void onClick(View v) { listener.onViewThread(position); } - }); + }; + content.setOnClickListener(viewThreadListener); + container.setOnClickListener(viewThreadListener); } public void setupWithStatus(Status status, StatusActionListener listener, int position) { setDisplayName(status.getDisplayName()); setUsername(status.getUsername()); setCreatedAt(status.getCreatedAt()); - setContent(status.getContent(), listener); + setContent(status.getContent(), status.getMentions(), listener); setAvatar(status.getAvatar()); setReblogged(status.getReblogged()); setFavourited(status.getFavourited()); diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index b8575baa..f6c375e4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -48,13 +48,14 @@ public class TimelineFragment extends SFragment implements MENTIONS, PUBLIC, TAG, + USER, } private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; private TimelineAdapter adapter; private Kind kind; - private String hashtag; + private String hashtagOrId; private LinearLayoutManager layoutManager; private EndlessOnScrollListener scrollListener; private TabLayout.OnTabSelectedListener onTabSelectedListener; @@ -67,11 +68,11 @@ public class TimelineFragment extends SFragment implements return fragment; } - public static TimelineFragment newInstance(Kind kind, String hashtag) { + public static TimelineFragment newInstance(Kind kind, String hashtagOrId) { TimelineFragment fragment = new TimelineFragment(); Bundle arguments = new Bundle(); arguments.putString("kind", kind.name()); - arguments.putString("hashtag", hashtag); + arguments.putString("hashtag_or_id", hashtagOrId); fragment.setArguments(arguments); return fragment; } @@ -82,8 +83,8 @@ public class TimelineFragment extends SFragment implements Bundle arguments = getArguments(); kind = Kind.valueOf(arguments.getString("kind")); - if (kind == Kind.TAG) { - hashtag = arguments.getString("hashtag"); + if (kind == Kind.TAG || kind == Kind.USER) { + hashtagOrId = arguments.getString("hashtag_or_id"); } View rootView = inflater.inflate(R.layout.fragment_timeline, container, false); @@ -118,7 +119,7 @@ public class TimelineFragment extends SFragment implements adapter = new TimelineAdapter(this, this); recyclerView.setAdapter(adapter); - if (kind != Kind.TAG) { + if (jumpToTopAllowed()) { TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); onTabSelectedListener = new TabLayout.OnTabSelectedListener() { @Override @@ -144,13 +145,17 @@ public class TimelineFragment extends SFragment implements @Override public void onDestroyView() { - if (kind != Kind.TAG) { + if (jumpToTopAllowed()) { TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout); tabLayout.removeOnTabSelectedListener(onTabSelectedListener); } super.onDestroyView(); } + private boolean jumpToTopAllowed() { + return kind != Kind.TAG; + } + private void jumpToTop() { layoutManager.scrollToPositionWithOffset(0, 0); scrollListener.reset(); @@ -173,8 +178,13 @@ public class TimelineFragment extends SFragment implements break; } case TAG: { - assert(hashtag != null); - endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtag); + assert(hashtagOrId != null); + endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtagOrId); + break; + } + case USER: { + assert(hashtagOrId != null); + endpoint = String.format(getString(R.string.endpoint_statuses), hashtagOrId); break; } } @@ -280,4 +290,15 @@ public class TimelineFragment extends SFragment implements public void onViewTag(String tag) { super.viewTag(tag); } + + public void onViewAccount(String id, String username) { + super.viewAccount(id, username); + } + + public void onViewAccount(int position) { + Status status = adapter.getItem(position); + String id = status.getAccountId(); + String username = status.getUsername(); + super.viewAccount(id, username); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java index ccc6e392..4e7333e7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java @@ -144,4 +144,15 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene public void onViewTag(String tag) { super.viewTag(tag); } + + public void onViewAccount(String id, String username) { + super.viewAccount(id, username); + } + + public void onViewAccount(int position) { + Status status = adapter.getItem(position); + String id = status.getAccountId(); + String username = status.getUsername(); + super.viewAccount(id, username); + } } diff --git a/app/src/main/res/drawable/account_header_default.png b/app/src/main/res/drawable/account_header_default.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e44ad25b2e13190a7570edcfbcf761b80f38c6 GIT binary patch literal 1891 zcmeAS@N?(olHy`uVBq!ia0y~yVA{jL!06Ax1{7HzBAW^17FM=u`STG-vy9r}lT zS7*J0086QmOFBEN;F2Sgmt9b@syJNz{qd7|)2B0YRK^;Oi~auj)n51CavJX#Ze}O^XPEJ< zCgrj8&-WSL@g>^7fx*vN;1OBOz@VoL!i*J5?aP3IVpSm#C5fda8Tmz^_$WT(t?8x9 z&cJ??DJe`$%7cM{wc69gF{I+w+iQ-33KGUbu1p6S zbeNlgp-r5Dfk&T#K_ZTU!2lTV2^EZR`oIS!hK7f%s0y)ZfGLF0SWQN@0Yf1!r+^fK zoP?nP=4KcTb33v^47VdY0h + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index dd1c538d..8d8ecbc3 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -66,6 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/status_since_created_left_margin" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index dedc9859..c5021b36 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -9,6 +9,11 @@ android:icon="@drawable/ic_compose" app:showAsAction="always" /> + + #DFDFDF #4F5F6F #9F9F9F + #FFFFFF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb471228..ff16b87c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,8 +5,6 @@ oauth2redirect com.keylesspalace.tusky.PREFERENCES - content://com.keylesspalace.tusky.viewtagactivity/%s - /api/v1/statuses /api/v1/media /api/v1/timelines/home @@ -42,13 +40,13 @@ Tusky failed to fetch the timeline. Notifications could not be fetched. - The toot is too long! - The toot failed to be sent. + The status is too long! + The status failed to be sent. The file must be less than 4MB. That type of file is not able to be uploaded. That file could not be opened. Permission to read media is required to upload it. - Images and videos cannot both be attached to the same toot. + Images and videos cannot both be attached to the same status. The media could not be uploaded. Home @@ -56,6 +54,9 @@ Public Thread #%s + Posts + Follows + Followers \@%s %s boosted @@ -64,21 +65,24 @@ Could not load the rest of the toots. - %s boosted your toot - %s favourited your toot + %s boosted your status + %s favourited your status %s followed you Compose Log In Log Out Follow + Unfollow Block + Unblock Delete TOOT Retry Mark Sensitive Cancel Back + Profile Toot!