Partial account profile pages now in. Follows/Followers tabs are empty and block/follow does nothing yet.

This commit is contained in:
Vavassor 2017-01-27 22:33:43 -05:00
parent dbb2663882
commit 60d68b0ae6
22 changed files with 791 additions and 65 deletions

View file

@ -35,6 +35,7 @@
<activity android:name=".ViewVideoActivity" /> <activity android:name=".ViewVideoActivity" />
<activity android:name=".ViewThreadActivity" /> <activity android:name=".ViewThreadActivity" />
<activity android:name=".ViewTagActivity" /> <activity android:name=".ViewTagActivity" />
<activity android:name=".AccountActivity" />
<service <service
android:name=".NotificationService" android:name=".NotificationService"
android:description="@string/notification_service_description" android:description="@string/notification_service_description"

View file

@ -0,0 +1,267 @@
/* 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
* <http://www.gnu.org/licenses/>. */
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<JSONObject>() {
@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<String, String> getHeaders() throws AuthFailureError {
Map<String, String> 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<JSONArray>() {
@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<String, String> getHeaders() throws AuthFailureError {
Map<String, String> 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);
}
}

View file

@ -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
* <http://www.gnu.org/licenses/>. */
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);
}
}

View file

@ -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
* <http://www.gnu.org/licenses/>. */
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];
}
}

View file

@ -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
* <http://www.gnu.org/licenses/>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
public interface AdapterItemRemover { public interface AdapterItemRemover {

View file

@ -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
* <http://www.gnu.org/licenses/>. */
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 </p> tag, which
* all status contents do, so it should be trimmed. */
return (Spanned) trimTrailingWhitespace(result);
}
}

View file

@ -29,16 +29,33 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; 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 { public class MainActivity extends AppCompatActivity {
private AlarmManager alarmManager; private AlarmManager alarmManager;
private PendingIntent serviceAlarmIntent; private PendingIntent serviceAlarmIntent;
private boolean notificationServiceEnabled; private boolean notificationServiceEnabled;
private String loggedInAccountId;
private String loggedInAccountUsername;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
// Fetch user info while we're doing other things.
fetchUserInfo();
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(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<JSONObject>() {
@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<String, String> getHeaders() throws AuthFailureError {
Map<String, String> 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() { private void compose() {
Intent intent = new Intent(this, ComposeActivity.class); Intent intent = new Intent(this, ComposeActivity.class);
startActivity(intent); 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() { private void logOut() {
if (notificationServiceEnabled) { if (notificationServiceEnabled) {
alarmManager.cancel(serviceAlarmIntent); alarmManager.cancel(serviceAlarmIntent);
@ -108,6 +192,10 @@ public class MainActivity extends AppCompatActivity {
compose(); compose();
return true; return true;
} }
case R.id.action_profile: {
viewProfile();
return true;
}
case R.id.action_logout: { case R.id.action_logout: {
logOut(); logOut();
return true; return true;

View file

@ -202,4 +202,15 @@ public class NotificationsFragment extends SFragment implements
public void onViewTag(String tag) { public void onViewTag(String tag) {
super.viewTag(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);
}
} }

View file

@ -33,7 +33,6 @@ import com.android.volley.Response;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest; import com.android.volley.toolbox.JsonObjectRequest;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList; import java.util.ArrayList;
@ -61,10 +60,12 @@ public class SFragment extends Fragment {
getString(R.string.preferences_file_key), Context.MODE_PRIVATE); getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
domain = preferences.getString("domain", null); domain = preferences.getString("domain", null);
accessToken = preferences.getString("accessToken", null); accessToken = preferences.getString("accessToken", null);
loggedInAccountId = preferences.getString("loggedInAccountId", null);
loggedInUsername = preferences.getString("loggedInAccountUsername", null);
assert(domain != null); assert(domain != null);
assert(accessToken != null); assert(accessToken != null);
assert(loggedInAccountId != null);
sendUserInfoRequest(); assert(loggedInUsername != null);
} }
protected void sendRequest( protected void sendRequest(
@ -100,22 +101,6 @@ public class SFragment extends Fragment {
sendRequest(Request.Method.POST, endpoint, null, null); sendRequest(Request.Method.POST, endpoint, null, null);
} }
private void sendUserInfoRequest() {
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null,
new Response.Listener<JSONObject>() {
@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) { protected void reply(Status status) {
String inReplyToId = status.getId(); String inReplyToId = status.getId();
Status.Mention[] mentions = status.getMentions(); Status.Mention[] mentions = status.getMentions();
@ -250,4 +235,11 @@ public class SFragment extends Fragment {
intent.putExtra("hashtag", tag); intent.putExtra("hashtag", tag);
startActivity(intent); 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);
}
} }

View file

@ -21,6 +21,13 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity; 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 { public class SplashActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {

View file

@ -182,26 +182,6 @@ public class Status {
return date; 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 </p> 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 { public static Status parse(JSONObject object, boolean isReblog) throws JSONException {
String id = object.getString("id"); String id = object.getString("id");
String content = object.getString("content"); String content = object.getString("content");
@ -264,7 +244,7 @@ public class Status {
status = reblog; status = reblog;
status.setRebloggedByUsername(username); status.setRebloggedByUsername(username);
} else { } else {
Spanned contentPlus = compatFromHtml(content); Spanned contentPlus = HtmlUtils.fromHtml(content);
status = new Status( status = new Status(
id, accountId, displayName, username, contentPlus, avatar, createdAt, id, accountId, displayName, username, contentPlus, avatar, createdAt,
reblogged, favourited, visibility); reblogged, favourited, visibility);

View file

@ -25,4 +25,6 @@ public interface StatusActionListener {
void onViewMedia(String url, Status.MediaAttachment.Type type); void onViewMedia(String url, Status.MediaAttachment.Type type);
void onViewThread(int position); void onViewThread(int position);
void onViewTag(String tag); void onViewTag(String tag);
void onViewAccount(String id, String username);
void onViewAccount(int position);
} }

View file

@ -98,7 +98,8 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
username.setText(usernameText); 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. // Redirect URLSpan's in the status content to the listener for viewing tag pages.
SpannableStringBuilder builder = new SpannableStringBuilder(content); SpannableStringBuilder builder = new SpannableStringBuilder(content);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); 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 start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span); int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span); int flags = builder.getSpanFlags(span);
CharSequence tag = builder.subSequence(start, end); CharSequence text = builder.subSequence(start, end);
if (tag.charAt(0) == '#') { if (text.charAt(0) == '#') {
final String viewTag = tag.subSequence(1, tag.length()).toString(); final String tag = text.subSequence(1, text.length()).toString();
ClickableSpan newSpan = new ClickableSpan() { ClickableSpan newSpan = new ClickableSpan() {
@Override @Override
public void onClick(View widget) { public void onClick(View widget) {
listener.onViewTag(viewTag); listener.onViewTag(tag);
} }
}; };
builder.removeSpan(span); builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags); 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. // Set the contents.
@ -236,7 +256,12 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
} }
public void setupButtons(final StatusActionListener listener, final int position) { 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() { replyButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -261,19 +286,25 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
listener.onMore(v, position); 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 @Override
public void onClick(View v) { public void onClick(View v) {
listener.onViewThread(position); listener.onViewThread(position);
} }
}); };
content.setOnClickListener(viewThreadListener);
container.setOnClickListener(viewThreadListener);
} }
public void setupWithStatus(Status status, StatusActionListener listener, int position) { public void setupWithStatus(Status status, StatusActionListener listener, int position) {
setDisplayName(status.getDisplayName()); setDisplayName(status.getDisplayName());
setUsername(status.getUsername()); setUsername(status.getUsername());
setCreatedAt(status.getCreatedAt()); setCreatedAt(status.getCreatedAt());
setContent(status.getContent(), listener); setContent(status.getContent(), status.getMentions(), listener);
setAvatar(status.getAvatar()); setAvatar(status.getAvatar());
setReblogged(status.getReblogged()); setReblogged(status.getReblogged());
setFavourited(status.getFavourited()); setFavourited(status.getFavourited());

View file

@ -48,13 +48,14 @@ public class TimelineFragment extends SFragment implements
MENTIONS, MENTIONS,
PUBLIC, PUBLIC,
TAG, TAG,
USER,
} }
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private TimelineAdapter adapter; private TimelineAdapter adapter;
private Kind kind; private Kind kind;
private String hashtag; private String hashtagOrId;
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener; private EndlessOnScrollListener scrollListener;
private TabLayout.OnTabSelectedListener onTabSelectedListener; private TabLayout.OnTabSelectedListener onTabSelectedListener;
@ -67,11 +68,11 @@ public class TimelineFragment extends SFragment implements
return fragment; return fragment;
} }
public static TimelineFragment newInstance(Kind kind, String hashtag) { public static TimelineFragment newInstance(Kind kind, String hashtagOrId) {
TimelineFragment fragment = new TimelineFragment(); TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putString("kind", kind.name()); arguments.putString("kind", kind.name());
arguments.putString("hashtag", hashtag); arguments.putString("hashtag_or_id", hashtagOrId);
fragment.setArguments(arguments); fragment.setArguments(arguments);
return fragment; return fragment;
} }
@ -82,8 +83,8 @@ public class TimelineFragment extends SFragment implements
Bundle arguments = getArguments(); Bundle arguments = getArguments();
kind = Kind.valueOf(arguments.getString("kind")); kind = Kind.valueOf(arguments.getString("kind"));
if (kind == Kind.TAG) { if (kind == Kind.TAG || kind == Kind.USER) {
hashtag = arguments.getString("hashtag"); hashtagOrId = arguments.getString("hashtag_or_id");
} }
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false); 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); adapter = new TimelineAdapter(this, this);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
if (kind != Kind.TAG) { if (jumpToTopAllowed()) {
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout); TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() { onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override @Override
@ -144,13 +145,17 @@ public class TimelineFragment extends SFragment implements
@Override @Override
public void onDestroyView() { public void onDestroyView() {
if (kind != Kind.TAG) { if (jumpToTopAllowed()) {
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout); TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
tabLayout.removeOnTabSelectedListener(onTabSelectedListener); tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
} }
super.onDestroyView(); super.onDestroyView();
} }
private boolean jumpToTopAllowed() {
return kind != Kind.TAG;
}
private void jumpToTop() { private void jumpToTop() {
layoutManager.scrollToPositionWithOffset(0, 0); layoutManager.scrollToPositionWithOffset(0, 0);
scrollListener.reset(); scrollListener.reset();
@ -173,8 +178,13 @@ public class TimelineFragment extends SFragment implements
break; break;
} }
case TAG: { case TAG: {
assert(hashtag != null); assert(hashtagOrId != null);
endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtag); 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; break;
} }
} }
@ -280,4 +290,15 @@ public class TimelineFragment extends SFragment implements
public void onViewTag(String tag) { public void onViewTag(String tag) {
super.viewTag(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);
}
} }

View file

@ -144,4 +144,15 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
public void onViewTag(String tag) { public void onViewTag(String tag) {
super.viewTag(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);
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="?attr/actionBarSize"
android:background="@color/account_header_background">
<com.android.volley.toolbox.NetworkImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/account_header"
android:scaleType="fitCenter"
android:adjustViewBounds="true" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.android.volley.toolbox.NetworkImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:id="@+id/account_avatar"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_toRightOf="@id/account_avatar"
android:layout_centerVertical="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_display_name"
android:textStyle="normal|bold"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_username" />
</LinearLayout>
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_note"
android:padding="8dp" />
</LinearLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:layout_gravity="top"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/pager"
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.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.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.design.widget.TabLayout>
</android.support.v4.view.ViewPager>
<FrameLayout
android:id="@+id/overlay_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.design.widget.CoordinatorLayout>

View file

@ -66,6 +66,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/status_since_created_left_margin" /> android:layout_marginLeft="@dimen/status_since_created_left_margin" />
</LinearLayout> </LinearLayout>
<TextView <TextView

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_back"
android:title="@string/action_back"
android:icon="@drawable/ic_back"
app:showAsAction="always" />
<item android:id="@+id/action_follow"
android:title="@string/action_follow"
app:showAsAction="never" />
<item android:id="@+id/action_block"
android:title="@string/action_block"
app:showAsAction="never" />
</menu>

View file

@ -9,6 +9,11 @@
android:icon="@drawable/ic_compose" android:icon="@drawable/ic_compose"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/action_profile"
android:title="@string/action_profile"
app:showAsAction="never" />
<item <item
android:id="@+id/action_logout" android:id="@+id/action_logout"
android:title="@string/action_logout" android:title="@string/action_logout"

View file

@ -9,4 +9,5 @@
<color name="media_preview_unloaded_background">#DFDFDF</color> <color name="media_preview_unloaded_background">#DFDFDF</color>
<color name="compose_mention">#4F5F6F</color> <color name="compose_mention">#4F5F6F</color>
<color name="notification_content_faded">#9F9F9F</color> <color name="notification_content_faded">#9F9F9F</color>
<color name="account_header_background">#FFFFFF</color>
</resources> </resources>

View file

@ -5,8 +5,6 @@
<string name="oauth_redirect_host">oauth2redirect</string> <string name="oauth_redirect_host">oauth2redirect</string>
<string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string> <string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string>
<string name="content_uri_format_tag">content://com.keylesspalace.tusky.viewtagactivity/%s</string>
<string name="endpoint_status">/api/v1/statuses</string> <string name="endpoint_status">/api/v1/statuses</string>
<string name="endpoint_media">/api/v1/media</string> <string name="endpoint_media">/api/v1/media</string>
<string name="endpoint_timelines_home">/api/v1/timelines/home</string> <string name="endpoint_timelines_home">/api/v1/timelines/home</string>
@ -42,13 +40,13 @@
<string name="error_fetching_timeline">Tusky failed to fetch the timeline.</string> <string name="error_fetching_timeline">Tusky failed to fetch the timeline.</string>
<string name="error_fetching_notifications">Notifications could not be fetched.</string> <string name="error_fetching_notifications">Notifications could not be fetched.</string>
<string name="error_compose_character_limit">The toot is too long!</string> <string name="error_compose_character_limit">The status is too long!</string>
<string name="error_sending_status">The toot failed to be sent.</string> <string name="error_sending_status">The status failed to be sent.</string>
<string name="error_media_upload_size">The file must be less than 4MB.</string> <string name="error_media_upload_size">The file must be less than 4MB.</string>
<string name="error_media_upload_type">That type of file is not able to be uploaded.</string> <string name="error_media_upload_type">That type of file is not able to be uploaded.</string>
<string name="error_media_upload_opening">That file could not be opened.</string> <string name="error_media_upload_opening">That file could not be opened.</string>
<string name="error_media_upload_permission">Permission to read media is required to upload it.</string> <string name="error_media_upload_permission">Permission to read media is required to upload it.</string>
<string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same toot.</string> <string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same status.</string>
<string name="error_media_upload_sending">The media could not be uploaded.</string> <string name="error_media_upload_sending">The media could not be uploaded.</string>
<string name="title_home">Home</string> <string name="title_home">Home</string>
@ -56,6 +54,9 @@
<string name="title_public">Public</string> <string name="title_public">Public</string>
<string name="title_thread">Thread</string> <string name="title_thread">Thread</string>
<string name="title_tag">#%s</string> <string name="title_tag">#%s</string>
<string name="title_statuses">Posts</string>
<string name="title_follows">Follows</string>
<string name="title_followers">Followers</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string> <string name="status_boosted_format">%s boosted</string>
@ -64,21 +65,24 @@
<string name="footer_text">Could not load the rest of the toots.</string> <string name="footer_text">Could not load the rest of the toots.</string>
<string name="notification_reblog_format">%s boosted your toot</string> <string name="notification_reblog_format">%s boosted your status</string>
<string name="notification_favourite_format">%s favourited your toot</string> <string name="notification_favourite_format">%s favourited your status</string>
<string name="notification_follow_format">%s followed you</string> <string name="notification_follow_format">%s followed you</string>
<string name="action_compose">Compose</string> <string name="action_compose">Compose</string>
<string name="action_login">Log In</string> <string name="action_login">Log In</string>
<string name="action_logout">Log Out</string> <string name="action_logout">Log Out</string>
<string name="action_follow">Follow</string> <string name="action_follow">Follow</string>
<string name="action_unfollow">Unfollow</string>
<string name="action_block">Block</string> <string name="action_block">Block</string>
<string name="action_unblock">Unblock</string>
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="action_send">TOOT</string> <string name="action_send">TOOT</string>
<string name="action_retry">Retry</string> <string name="action_retry">Retry</string>
<string name="action_mark_sensitive">Mark Sensitive</string> <string name="action_mark_sensitive">Mark Sensitive</string>
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_back">Back</string> <string name="action_back">Back</string>
<string name="action_profile">Profile</string>
<string name="confirmation_send">Toot!</string> <string name="confirmation_send">Toot!</string>