Merge branch 'Gargron-master'

This commit is contained in:
Vavassor 2017-03-09 17:04:16 -05:00
commit 0ed8691057
67 changed files with 1405 additions and 1635 deletions

View File

@ -35,4 +35,10 @@ dependencies {
compile 'com.github.peter9870:sparkbutton:master'
testCompile 'junit:junit:4.12'
compile 'com.mikhaellopez:circularfillableloaders:1.2.0'
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile('com.mikepenz:materialdrawer:5.8.2@aar') {
transitive = true
}
compile 'com.github.chrisbanes:PhotoView:1.3.1'
}

View File

@ -1,92 +0,0 @@
/* 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.text.Spanned;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
class Account {
String id;
String username;
String displayName;
Spanned note;
String url;
String avatar;
String header;
String followersCount;
String followingCount;
String statusesCount;
public static Account parse(JSONObject object) throws JSONException {
Account account = new Account();
account.id = object.getString("id");
account.username = object.getString("acct");
account.displayName = object.getString("display_name");
if (account.displayName.isEmpty()) {
account.displayName = object.getString("username");
}
account.note = HtmlUtils.fromHtml(object.getString("note"));
account.url = object.getString("url");
String avatarUrl = object.getString("avatar");
if (!avatarUrl.equals("/avatars/original/missing.png")) {
account.avatar = avatarUrl;
} else {
account.avatar = null;
}
String headerUrl = object.getString("header");
if (!headerUrl.equals("/headers/original/missing.png")) {
account.header = headerUrl;
} else {
account.header = null;
}
account.followersCount = object.getString("followers_count");
account.followingCount = object.getString("following_count");
account.statusesCount = object.getString("statuses_count");
return account;
}
public static List<Account> parse(JSONArray array) throws JSONException {
List<Account> accounts = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
JSONObject object = array.getJSONObject(i);
Account account = parse(object);
accounts.add(account);
}
return accounts;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Account)) {
return false;
}
Account account = (Account) other;
return account.id.equals(this.id);
}
}

View File

@ -39,30 +39,25 @@ import android.view.View;
import android.widget.ImageView;
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.JsonArrayRequest;
import com.android.volley.toolbox.JsonObjectRequest;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AccountActivity extends BaseActivity {
private static final String TAG = "AccountActivity"; // Volley request tag and logging tag
private String domain;
private String accessToken;
private String accountId;
private boolean following = false;
private boolean blocking = false;
private boolean muting = false;
private boolean isSelf;
private String openInWebUrl;
private TabLayout tabLayout;
@ -77,8 +72,6 @@ public class AccountActivity extends BaseActivity {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
domain = preferences.getString("domain", null);
accessToken = preferences.getString("accessToken", null);
String loggedInAccountId = preferences.getString("loggedInAccountId", null);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
@ -169,37 +162,17 @@ public class AccountActivity extends BaseActivity {
}
private void obtainAccount() {
String endpoint = String.format(getString(R.string.endpoint_accounts), accountId);
String url = "https://" + domain + endpoint;
JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Account account;
try {
account = Account.parse(response);
} catch (JSONException e) {
onObtainAccountFailure();
return;
}
onObtainAccountSuccess(account);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onObtainAccountFailure();
}
}) {
mastodonAPI.account(accountId).enqueue(new Callback<Account>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Account> call, retrofit2.Response<Account> response) {
onObtainAccountSuccess(response.body());
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<Account> call, Throwable t) {
onObtainAccountFailure();
}
});
}
private void onObtainAccountSuccess(Account account) {
@ -263,47 +236,28 @@ public class AccountActivity extends BaseActivity {
}
private void obtainRelationships() {
String endpoint = getString(R.string.endpoint_relationships);
String url = String.format("https://%s%s?id=%s", domain, endpoint, accountId);
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(e);
return;
}
onObtainRelationshipsSuccess(following, blocking);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onObtainRelationshipsFailure(error);
}
}) {
List<String> ids = new ArrayList<>(1);
ids.add(accountId);
mastodonAPI.relationships(ids).enqueue(new Callback<List<Relationship>>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<List<Relationship>> call, retrofit2.Response<List<Relationship>> response) {
Relationship relationship = response.body().get(0);
onObtainRelationshipsSuccess(relationship.following, relationship.blocking, relationship.muting);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<List<Relationship>> call, Throwable t) {
onObtainRelationshipsFailure((Exception) t);
}
});
}
private void onObtainRelationshipsSuccess(boolean following, boolean blocking) {
private void onObtainRelationshipsSuccess(boolean following, boolean blocking, boolean muting) {
this.following = following;
this.blocking = blocking;
this.muting = muting;
if (!following || !blocking) {
if (!following || !blocking || !muting) {
invalidateOptionsMenu();
}
@ -355,58 +309,42 @@ public class AccountActivity extends BaseActivity {
title = getString(R.string.action_block);
}
block.setTitle(title);
MenuItem mute = menu.findItem(R.id.action_mute);
if (muting) {
title = getString(R.string.action_unmute);
} else {
title = getString(R.string.action_mute);
}
mute.setTitle(title);
} else {
// It shouldn't be possible to block or follow yourself.
menu.removeItem(R.id.action_follow);
menu.removeItem(R.id.action_block);
menu.removeItem(R.id.action_mute);
}
return super.onPrepareOptionsMenu(menu);
}
private void postRequest(String endpoint, Response.Listener<JSONObject> listener,
Response.ErrorListener errorListener) {
String url = "https://" + domain + endpoint;
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, null, listener,
errorListener) {
private void follow(final String id) {
Callback<Relationship> cb = new Callback<Relationship>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
following = response.body().following;
// TODO: display message/indicator when "requested" is true (i.e. when the follow is awaiting approval)
updateButtons();
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onFollowFailure(id);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
}
private void follow(final String id) {
int endpointId;
if (following) {
endpointId = R.string.endpoint_unfollow;
mastodonAPI.unfollowAccount(id).enqueue(cb);
} else {
endpointId = R.string.endpoint_follow;
mastodonAPI.followAccount(id).enqueue(cb);
}
postRequest(String.format(getString(endpointId), id),
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
boolean followingValue;
try {
followingValue = response.getBoolean("following");
} catch (JSONException e) {
onFollowFailure(id);
return;
}
following = followingValue;
updateButtons();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFollowFailure(id);
}
});
}
private void onFollowFailure(final String id) {
@ -428,33 +366,23 @@ public class AccountActivity extends BaseActivity {
}
private void block(final String id) {
int endpointId;
Callback<Relationship> cb = new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
blocking = response.body().blocking;
updateButtons();
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onBlockFailure(id);
}
};
if (blocking) {
endpointId = R.string.endpoint_unblock;
mastodonAPI.unblockAccount(id).enqueue(cb);
} else {
endpointId = R.string.endpoint_block;
mastodonAPI.blockAccount(id).enqueue(cb);
}
postRequest(String.format(getString(endpointId), id),
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
boolean blockingValue;
try {
blockingValue = response.getBoolean("blocking");
} catch (JSONException e) {
onBlockFailure(id);
return;
}
blocking = blockingValue;
updateButtons();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onBlockFailure(id);
}
});
}
private void onBlockFailure(final String id) {
@ -475,6 +403,50 @@ public class AccountActivity extends BaseActivity {
.show();
}
private void mute(final String id) {
Callback<Relationship> cb = new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
muting = response.body().muting;
updateButtons();
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onMuteFailure(id);
}
};
if (muting) {
mastodonAPI.unmuteAccount(id).enqueue(cb);
} else {
mastodonAPI.muteAccount(id).enqueue(cb);
}
}
private void onMuteFailure(final String id) {
int messageId;
if (muting) {
messageId = R.string.error_unmuting;
} else {
messageId = R.string.error_muting;
}
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mute(id);
}
};
Snackbar.make(findViewById(R.id.activity_account), messageId, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry, listener)
.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -496,6 +468,10 @@ public class AccountActivity extends BaseActivity {
block(accountId);
return true;
}
case R.id.action_mute: {
mute(accountId);
return true;
}
}
return super.onOptionsItemSelected(item);
}

View File

@ -17,6 +17,8 @@ package com.keylesspalace.tusky;
import android.support.v7.widget.RecyclerView;
import com.keylesspalace.tusky.entity.Account;
import java.util.ArrayList;
import java.util.List;

View File

@ -34,16 +34,17 @@ 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.JsonArrayRequest;
import com.android.volley.toolbox.StringRequest;
import org.json.JSONArray;
import org.json.JSONException;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
public class AccountFragment extends Fragment implements AccountActionListener,
FooterActionListener {
private static final String TAG = "Account"; // logging tag and Volley request tag
@ -63,6 +64,7 @@ public class AccountFragment extends Fragment implements AccountActionListener,
private EndlessOnScrollListener scrollListener;
private AccountAdapter adapter;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
private MastodonAPI api;
public static AccountFragment newInstance(Type type) {
Bundle arguments = new Bundle();
@ -92,6 +94,7 @@ public class AccountFragment extends Fragment implements AccountActionListener,
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
domain = preferences.getString("domain", null);
accessToken = preferences.getString("accessToken", null);
api = ((BaseActivity) getActivity()).mastodonAPI;
}
@Override
@ -170,55 +173,33 @@ public class AccountFragment extends Fragment implements AccountActionListener,
}
private void fetchAccounts(final String fromId) {
String endpoint;
Callback<List<Account>> cb = new Callback<List<Account>>() {
@Override
public void onResponse(Call<List<Account>> call, retrofit2.Response<List<Account>> response) {
onFetchAccountsSuccess(response.body(), fromId);
}
@Override
public void onFailure(Call<List<Account>> call, Throwable t) {
onFetchAccountsFailure((Exception) t);
}
};
switch (type) {
default:
case FOLLOWS: {
endpoint = String.format(getString(R.string.endpoint_following), accountId);
api.accountFollowing(accountId, fromId, null, null).enqueue(cb);
break;
}
case FOLLOWERS: {
endpoint = String.format(getString(R.string.endpoint_followers), accountId);
api.accountFollowers(accountId, fromId, null, null).enqueue(cb);
break;
}
case BLOCKS: {
endpoint = getString(R.string.endpoint_blocks);
api.blocks(fromId, null, null).enqueue(cb);
break;
}
}
String url = "https://" + domain + endpoint;
if (fromId != null) {
url += "?max_id=" + fromId;
}
JsonArrayRequest request = new JsonArrayRequest(url,
new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
List<Account> accounts;
try {
accounts = Account.parse(response);
} catch (JSONException e) {
onFetchAccountsFailure(e);
return;
}
onFetchAccountsSuccess(accounts, fromId);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFetchAccountsFailure(error);
}
}) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
}
};
request.setTag(TAG);
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
}
private void fetchAccounts() {
@ -285,35 +266,23 @@ public class AccountFragment extends Fragment implements AccountActionListener,
}
public void onBlock(final boolean block, final String id, final int position) {
String endpoint;
if (!block) {
endpoint = String.format(getString(R.string.endpoint_unblock), id);
} else {
endpoint = String.format(getString(R.string.endpoint_block), id);
}
String url = "https://" + domain + endpoint;
StringRequest request = new StringRequest(Request.Method.POST, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
onBlockSuccess(block, position);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onBlockFailure(block, id);
}
}) {
Callback<Relationship> cb = new Callback<Relationship>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
onBlockSuccess(block, position);
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onBlockFailure(block, id);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
if (!block) {
api.unblockAccount(id).enqueue(cb);
} else {
api.blockAccount(id).enqueue(cb);
}
}
private void onBlockSuccess(boolean blocked, int position) {

View File

@ -15,7 +15,9 @@
package com.keylesspalace.tusky;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
@ -23,17 +25,35 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.Spanned;
import android.util.TypedValue;
import android.view.Menu;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/* There isn't presently a way to globally change the theme of a whole application at runtime, just
* individual activities. So, each activity has to set its theme before any views are created. And
* the most expedient way to accomplish this was to put it in a base class and just have every
* activity extend from it. */
public class BaseActivity extends AppCompatActivity {
protected MastodonAPI mastodonAPI;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
createMastodonAPI();
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
@ -59,6 +79,46 @@ public class BaseActivity extends AppCompatActivity {
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
}
protected String getAccessToken() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
return preferences.getString("accessToken", null);
}
protected String getBaseUrl() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
return "https://" + preferences.getString("domain", null);
}
protected void createMastodonAPI() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder()
.header("Authorization", String.format("Bearer %s", getAccessToken()));
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
})
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getBaseUrl())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
mastodonAPI = retrofit.create(MastodonAPI.class);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
TypedValue value = new TypedValue();

View File

@ -21,6 +21,7 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
public class BlocksActivity extends BaseActivity {
@Override
@ -33,6 +34,8 @@ public class BlocksActivity extends BaseActivity {
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setTitle(getString(R.string.title_blocks));
bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true);
}
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
@ -40,4 +43,15 @@ public class BlocksActivity extends BaseActivity {
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -23,6 +23,7 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Account;
import com.squareup.picasso.Picasso;
import java.util.HashSet;

View File

@ -76,6 +76,8 @@ import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Status;
import org.json.JSONArray;
import org.json.JSONException;
@ -95,6 +97,12 @@ import java.util.Locale;
import java.util.Map;
import java.util.Random;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
public class ComposeActivity extends BaseActivity {
private static final String TAG = "ComposeActivity"; // logging tag, and volley request tag
private static final int STATUS_CHARACTER_LIMIT = 500;
@ -137,7 +145,7 @@ public class ComposeActivity extends BaseActivity {
ImageView preview;
Uri uri;
String id;
Request uploadRequest;
Call<Media> uploadRequest;
ReadyStage readyStage;
byte[] content;
long mediaSize;
@ -629,53 +637,28 @@ public class ComposeActivity extends BaseActivity {
private void sendStatus(String content, String visibility, boolean sensitive,
String spoilerText) {
String endpoint = getString(R.string.endpoint_status);
String url = "https://" + domain + endpoint;
JSONObject parameters = new JSONObject();
try {
parameters.put("status", content);
parameters.put("visibility", visibility);
parameters.put("sensitive", sensitive);
parameters.put("spoiler_text", spoilerText);
if (inReplyToId != null) {
parameters.put("in_reply_to_id", inReplyToId);
}
JSONArray mediaIds = new JSONArray();
for (QueuedMedia item : mediaQueued) {
mediaIds.put(item.id);
}
if (mediaIds.length() > 0) {
parameters.put("media_ids", mediaIds);
}
} catch (JSONException e) {
onSendFailure();
return;
ArrayList<String> mediaIds = new ArrayList<String>();
for (QueuedMedia item : mediaQueued) {
mediaIds.add(item.id);
}
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
onSendSuccess();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onSendFailure();
}
}) {
mastodonAPI.createStatus(content, inReplyToId, spoilerText, visibility, sensitive, mediaIds).enqueue(new Callback<Status>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
onSendSuccess();
}
};
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<Status> call, Throwable t) {
onSendFailure();
}
});
}
private void onSendSuccess() {
Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show();
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT);
bar.show();
finish();
}
@ -941,9 +924,6 @@ public class ComposeActivity extends BaseActivity {
private void uploadMedia(final QueuedMedia item) {
item.readyStage = QueuedMedia.ReadyStage.UPLOADING;
String endpoint = getString(R.string.endpoint_media);
String url = "https://" + domain + endpoint;
final String mimeType = getContentResolver().getType(item.uri);
MimeTypeMap map = MimeTypeMap.getSingleton();
String fileExtension = map.getExtensionFromMimeType(mimeType);
@ -953,58 +933,42 @@ public class ComposeActivity extends BaseActivity {
randomAlphanumericString(10),
fileExtension);
MultipartRequest request = new MultipartRequest(Request.Method.POST, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
item.id = response.getString("id");
} catch (JSONException e) {
onUploadFailure(item);
return;
}
waitForMediaLatch.countDown();
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onUploadFailure(item);
}
}) {
byte[] content = item.content;
if (content == null) {
InputStream stream;
try {
stream = getContentResolver().openInputStream(item.uri);
} catch (FileNotFoundException e) {
return;
}
content = inputStreamGetBytes(stream);
IOUtils.closeQuietly(stream);
if (content == null) {
return;
}
}
RequestBody requestFile = RequestBody.create(MediaType.parse(mimeType), content);
MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile);
item.uploadRequest = mastodonAPI.uploadMedia(body);
item.uploadRequest.enqueue(new Callback<Media>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Media> call, retrofit2.Response<Media> response) {
item.id = response.body().id;
waitForMediaLatch.countDown();
}
@Override
public DataItem getData() {
byte[] content = item.content;
if (content == null) {
InputStream stream;
try {
stream = getContentResolver().openInputStream(item.uri);
} catch (FileNotFoundException e) {
return null;
}
content = inputStreamGetBytes(stream);
IOUtils.closeQuietly(stream);
if (content == null) {
return null;
}
}
DataItem data = new DataItem();
data.name = "file";
data.filename = filename;
data.mimeType = mimeType;
data.content = content;
return data;
public void onFailure(Call<Media> call, Throwable t) {
onUploadFailure(item);
}
};
request.setTag(TAG);
item.uploadRequest = request;
VolleySingleton.getInstance(this).addToRequestQueue(request);
});
}
private void onUploadFailure(QueuedMedia item) {

View File

@ -21,6 +21,7 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
public class FavouritesActivity extends BaseActivity {
@Override
@ -33,6 +34,8 @@ public class FavouritesActivity extends BaseActivity {
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setTitle(getString(R.string.title_favourites));
bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true);
}
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
@ -40,4 +43,15 @@ public class FavouritesActivity extends BaseActivity {
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -23,6 +23,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Account;
import com.squareup.picasso.Picasso;
/** Both for follows and following lists. */

View File

@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.preference.PreferenceManager;
@ -35,12 +36,26 @@ import android.transition.TransitionInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
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 com.keylesspalace.tusky.entity.Account;
import com.mikepenz.materialdrawer.AccountHeader;
import com.mikepenz.materialdrawer.AccountHeaderBuilder;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
import com.squareup.picasso.Picasso;
import org.json.JSONException;
import org.json.JSONObject;
@ -49,6 +64,9 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import retrofit2.Call;
import retrofit2.Callback;
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity"; // logging tag and Volley request tag
@ -59,6 +77,8 @@ public class MainActivity extends BaseActivity {
private String loggedInAccountUsername;
Stack<Integer> pageHistory = new Stack<Integer>();
private ViewPager viewPager;
private AccountHeader headerResult;
private Drawer drawer;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -80,6 +100,88 @@ public class MainActivity extends BaseActivity {
}
});
headerResult = new AccountHeaderBuilder()
.withActivity(this)
.withSelectionListEnabledForSingleProfile(false)
.withTranslucentStatusBar(true)
.withCompactStyle(true)
.withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
@Override
public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
intent.putExtra("id", loggedInAccountId);
startActivity(intent);
return false;
}
@Override
public boolean onProfileImageLongClick(View view, IProfile profile, boolean current) {
return false;
}
})
.build();
DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override
public void set(ImageView imageView, Uri uri, Drawable placeholder) {
Picasso.with(imageView.getContext()).load(uri).placeholder(placeholder).into(imageView);
}
@Override
public void cancel(ImageView imageView) {
Picasso.with(imageView.getContext()).cancelRequest(imageView);
}
});
drawer = new DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withTranslucentStatusBar(true)
.withAccountHeader(headerResult)
.withHasStableIds(true)
.withSelectedItem(-1)
.addDrawerItems(
new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withSelectable(false),
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_blocks)).withSelectable(false),
new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_preferences)).withSelectable(false),
new PrimaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_logout)).withSelectable(false)
)
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
@Override
public boolean onItemClick(View view, int position, IDrawerItem drawerItem) {
if (drawerItem != null) {
long drawerItemIdentifier = drawerItem.getIdentifier();
if (drawerItemIdentifier == 1) {
Intent intent = new Intent(MainActivity.this, FavouritesActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 2) {
Intent intent = new Intent(MainActivity.this, BlocksActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 3) {
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 4) {
if (notificationServiceEnabled) {
alarmManager.cancel(serviceAlarmIntent);
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.remove("domain");
editor.remove("accessToken");
editor.apply();
Intent intent = new Intent(MainActivity.this, SplashActivity.class);
startActivity(intent);
finish();
}
}
return false;
}
})
.build();
// Setup the tabs and timeline pager.
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
String[] pageTitles = {
@ -148,48 +250,42 @@ public class MainActivity extends BaseActivity {
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);
final String domain = preferences.getString("domain", 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) {
String username;
String id;
try {
id = response.getString("id");
username = response.getString("acct");
} catch (JSONException e) {
onFetchUserInfoFailure(e);
return;
}
onFetchUserInfoSuccess(id, username);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFetchUserInfoFailure(error);
}
}) {
//if (id != null && username != null) {
// loggedInAccountId = id;
// loggedInAccountUsername = username;
//} else {
mastodonAPI.accountVerifyCredentials().enqueue(new Callback<Account>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<Account> call, retrofit2.Response<Account> response) {
Account me = response.body();
ImageView background = headerResult.getHeaderBackgroundView();
Picasso.with(MainActivity.this)
.load(me.header)
.placeholder(R.drawable.account_header_missing)
.resize(background.getWidth(), background.getHeight())
.centerCrop()
.into(background);
headerResult.addProfiles(
new ProfileDrawerItem()
.withName(me.displayName)
.withEmail(String.format("%s@%s", me.username, domain))
.withIcon(me.avatar)
);
//onFetchUserInfoSuccess(response.body().id, response.body().username);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
}
@Override
public void onFailure(Call<Account> call, Throwable t) {
onFetchUserInfoFailure((Exception) t);
}
});
//}
}
private void onFetchUserInfoSuccess(String id, String username) {
@ -207,59 +303,11 @@ public class MainActivity extends BaseActivity {
Log.e(TAG, "Failed to fetch user info. " + exception.getMessage());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_toolbar, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_view_profile: {
Intent intent = new Intent(this, AccountActivity.class);
intent.putExtra("id", loggedInAccountId);
startActivity(intent);
return true;
}
case R.id.action_view_preferences: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivity(intent);
return true;
}
case R.id.action_view_favourites: {
Intent intent = new Intent(this, FavouritesActivity.class);
startActivity(intent);
return true;
}
case R.id.action_view_blocks: {
Intent intent = new Intent(this, BlocksActivity.class);
startActivity(intent);
return true;
}
case R.id.action_logout: {
if (notificationServiceEnabled) {
alarmManager.cancel(serviceAlarmIntent);
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.remove("domain");
editor.remove("accessToken");
editor.apply();
Intent intent = new Intent(this, SplashActivity.class);
startActivity(intent);
finish();
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
if(pageHistory.empty()) {
if(drawer != null && drawer.isDrawerOpen()) {
drawer.closeDrawer();
} else if(pageHistory.empty()) {
super.onBackPressed();
} else {
pageHistory.pop();

View File

@ -0,0 +1,170 @@
package com.keylesspalace.tusky;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonAPI {
@GET("api/v1/timelines/home")
Call<List<Status>> homeTimeline(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/timelines/public")
Call<List<Status>> publicTimeline(
@Query("local") Boolean local,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/timelines/tag/{hashtag}")
Call<List<Status>> hashtagTimeline(
@Path("hashtag") String hashtag,
@Query("local") Boolean local,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/notifications")
Call<List<Notification>> notifications(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@POST("api/v1/notifications/clear")
Call<ResponseBody> clearNotifications();
@GET("api/v1/notifications/{id}")
Call<Notification> notification(@Path("id") String notificationId);
@Multipart
@POST("api/v1/media")
Call<Media> uploadMedia(@Part("file") MultipartBody.Part file);
@FormUrlEncoded
@POST("api/v1/statuses")
Call<Status> createStatus(
@Field("status") String text,
@Field("in_reply_to_id") String inReplyToId,
@Field("spoiler_text") String warningText,
@Field("visibility") String visibility,
@Field("sensitive") Boolean sensitive,
@Field("media_ids[]") List<String> mediaIds);
@GET("api/v1/statuses/{id}")
Call<Status> status(@Path("id") String statusId);
@GET("api/v1/statuses/{id}/context")
Call<StatusContext> statusContext(@Path("id") String statusId);
@GET("api/v1/statuses/{id}/reblogged_by")
Call<List<Account>> statusRebloggedBy(
@Path("id") String statusId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/statuses/{id}/favourited_by")
Call<List<Account>> statusFavouritedBy(
@Path("id") String statusId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@DELETE("api/v1/statuses/{id}")
Call<ResponseBody> deleteStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/reblog")
Call<Status> reblogStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/unreblog")
Call<Status> unreblogStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/favourite")
Call<Status> favouriteStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/unfavourite")
Call<Status> unfavouriteStatus(@Path("id") String statusId);
@GET("api/v1/accounts/verify_credentials")
Call<Account> accountVerifyCredentials();
@GET("api/v1/accounts/search")
Call<List<Account>> searchAccounts(
@Query("q") String q,
@Query("resolve") Boolean resolve,
@Query("limit") Integer limit);
@GET("api/v1/accounts/{id}")
Call<Account> account(@Path("id") String accountId);
@GET("api/v1/accounts/{id}/statuses")
Call<List<Status>> accountStatuses(
@Path("id") String accountId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/accounts/{id}/followers")
Call<List<Account>> accountFollowers(
@Path("id") String accountId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/accounts/{id}/following")
Call<List<Account>> accountFollowing(
@Path("id") String accountId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@POST("api/v1/accounts/{id}/follow")
Call<Relationship> followAccount(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/unfollow")
Call<Relationship> unfollowAccount(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/block")
Call<Relationship> blockAccount(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/unblock")
Call<Relationship> unblockAccount(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/mute")
Call<Relationship> muteAccount(@Path("id") String accountId);
@POST("api/v1/accounts/{id}/unmute")
Call<Relationship> unmuteAccount(@Path("id") String accountId);
@GET("api/v1/accounts/relationships")
Call<List<Relationship>> relationships(@Query("id[]") List<String> accountIds);
@GET("api/v1/blocks")
Call<List<Account>> blocks(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/mutes")
Call<List<Account>> mutes(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/favourites")
Call<List<Status>> favourites(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/follow_requests")
Call<List<Account>> followRequests(
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@POST("api/v1/follow_requests/{id}/authorize")
Call<Relationship> authorizeFollowRequest(@Path("id") String accountId);
@POST("api/v1/follow_requests/{id}/reject")
Call<Relationship> rejectFollowRequest(@Path("id") String accountId);
@FormUrlEncoded
@POST("api/v1/reports")
Call<ResponseBody> report(@Field("account_id") String accountId, @Field("status_ids[]") List<String> statusIds, @Field("comment") String comment);
}

View File

@ -1,133 +0,0 @@
/* 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.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
class Notification {
enum Type {
MENTION,
REBLOG,
FAVOURITE,
FOLLOW,
}
private Type type;
private String id;
private String displayName;
private String username;
private String avatar;
private String accountId;
/** Which of the user's statuses has been mentioned, reblogged, or favourited. */
private Status status;
private Notification(Type type, String id, String displayName, String username, String avatar,
String accountId) {
this.type = type;
this.id = id;
this.displayName = displayName;
this.username = username;
this.avatar = avatar;
this.accountId = accountId;
}
Type getType() {
return type;
}
String getId() {
return id;
}
String getDisplayName() {
return displayName;
}
String getUsername() {
return username;
}
String getAvatar() {
return avatar;
}
String getAccountId() {
return accountId;
}
@Nullable Status getStatus() {
return status;
}
void setStatus(Status status) {
this.status = status;
}
private boolean hasStatusType() {
return type == Type.MENTION
|| type == Type.FAVOURITE
|| type == Type.REBLOG;
}
static List<Notification> parse(JSONArray array) throws JSONException {
List<Notification> notifications = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
JSONObject object = array.getJSONObject(i);
String id = object.getString("id");
Notification.Type type = Notification.Type.valueOf(
object.getString("type").toUpperCase());
JSONObject account = object.getJSONObject("account");
String displayName = account.getString("display_name");
if (displayName.isEmpty()) {
displayName = account.getString("username");
}
String username = account.getString("acct");
String avatar = account.getString("avatar");
String accountId = account.getString("id");
Notification notification = new Notification(type, id, displayName, username, avatar,
accountId);
if (notification.hasStatusType()) {
JSONObject statusObject = object.getJSONObject("status");
Status status = Status.parse(statusObject, false);
notification.setStatus(status);
}
notifications.add(notification);
}
return notifications;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Notification)) {
return false;
}
Notification notification = (Notification) other;
return notification.getId().equals(this.id);
}
}

View File

@ -28,6 +28,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status;
import com.squareup.picasso.Picasso;
import java.util.ArrayList;
@ -86,26 +88,26 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < notifications.size()) {
Notification notification = notifications.get(position);
Notification.Type type = notification.getType();
Notification.Type type = notification.type;
switch (type) {
case MENTION: {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
Status status = notification.getStatus();
Status status = notification.status;
holder.setupWithStatus(status, statusListener);
break;
}
case FAVOURITE:
case REBLOG: {
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
holder.setMessage(type, notification.getDisplayName(),
notification.getStatus());
holder.setMessage(type, notification.account.displayName,
notification.status);
break;
}
case FOLLOW: {
FollowViewHolder holder = (FollowViewHolder) viewHolder;
holder.setMessage(notification.getDisplayName(), notification.getUsername(),
notification.getAvatar());
holder.setupButtons(followListener, notification.getAccountId());
holder.setMessage(notification.account.displayName, notification.account.username,
notification.account.avatar);
holder.setupButtons(followListener, notification.account.id);
break;
}
}
@ -126,7 +128,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
return VIEW_TYPE_FOOTER;
} else {
Notification notification = notifications.get(position);
switch (notification.getType()) {
switch (notification.type) {
default:
case MENTION: {
return VIEW_TYPE_MENTION;
@ -269,7 +271,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
str.setSpan(new android.text.style.StyleSpan(Typeface.BOLD), 0, displayName.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
message.setText(str);
statusContent.setText(status.getContent());
statusContent.setText(status.content);
}
}
}

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@ -32,6 +33,8 @@ import com.android.volley.AuthFailureError;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status;
import org.json.JSONArray;
import org.json.JSONException;
@ -40,6 +43,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener,
NotificationsAdapter.FollowListener {
@ -65,6 +71,14 @@ public class NotificationsFragment extends SFragment implements
super.onDestroy();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
NotificationManager notificationManager =
(NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(PullNotificationService.NOTIFY_ID);
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@ -92,7 +106,7 @@ public class NotificationsFragment extends SFragment implements
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
if (notification != null) {
sendFetchNotificationsRequest(notification.getId());
sendFetchNotificationsRequest(notification.id);
} else {
sendFetchNotificationsRequest();
}
@ -135,37 +149,19 @@ public class NotificationsFragment extends SFragment implements
}
private void sendFetchNotificationsRequest(final String fromId) {
String endpoint = getString(R.string.endpoint_notifications);
String url = "https://" + domain + endpoint;
if (fromId != null) {
url += "?max_id=" + fromId;
}
JsonArrayRequest request = new JsonArrayRequest(url,
new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
try {
List<Notification> notifications = Notification.parse(response);
onFetchNotificationsSuccess(notifications, fromId);
} catch (JSONException e) {
onFetchNotificationsFailure(e);
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFetchNotificationsFailure(error);
}
}) {
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
api.notifications(fromId, null, null).enqueue(new Callback<List<Notification>>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
onFetchNotificationsSuccess(response.body(), fromId);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
@Override
public void onFailure(Call<List<Notification>> call, Throwable t) {
onFetchNotificationsFailure((Exception) t);
}
});
}
private void sendFetchNotificationsRequest() {
@ -174,7 +170,7 @@ public class NotificationsFragment extends SFragment implements
private static boolean findNotification(List<Notification> notifications, String id) {
for (Notification notification : notifications) {
if (notification.getId().equals(id)) {
if (notification.id.equals(id)) {
return true;
}
}
@ -218,7 +214,7 @@ public class NotificationsFragment extends SFragment implements
public void onLoadMore() {
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
if (notification != null) {
sendFetchNotificationsRequest(notification.getId());
sendFetchNotificationsRequest(notification.id);
} else {
sendFetchNotificationsRequest();
}
@ -226,22 +222,22 @@ public class NotificationsFragment extends SFragment implements
public void onReply(int position) {
Notification notification = adapter.getItem(position);
super.reply(notification.getStatus());
super.reply(notification.status);
}
public void onReblog(boolean reblog, int position) {
Notification notification = adapter.getItem(position);
super.reblog(notification.getStatus(), reblog, adapter, position);
super.reblog(notification.status, reblog, adapter, position);
}
public void onFavourite(boolean favourite, int position) {
Notification notification = adapter.getItem(position);
super.favourite(notification.getStatus(), favourite, adapter, position);
super.favourite(notification.status, favourite, adapter, position);
}
public void onMore(View view, int position) {
Notification notification = adapter.getItem(position);
super.more(notification.getStatus(), view, adapter, position);
super.more(notification.status, view, adapter, position);
}
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
@ -250,7 +246,7 @@ public class NotificationsFragment extends SFragment implements
public void onViewThread(int position) {
Notification notification = adapter.getItem(position);
super.viewThread(notification.getStatus());
super.viewThread(notification.status);
}
public void onViewTag(String tag) {

View File

@ -22,7 +22,7 @@ import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
public class PreferencesActivity extends AppCompatActivity
public class PreferencesActivity extends BaseActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
private boolean themeSwitched;

View File

@ -26,24 +26,38 @@ import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.Spanned;
import com.android.volley.AuthFailureError;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageRequest;
import com.android.volley.toolbox.JsonArrayRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.*;
import com.keylesspalace.tusky.entity.Notification;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class PullNotificationService extends IntentService {
private static final int NOTIFY_ID = 6; // This is an arbitrary number.
static final int NOTIFY_ID = 6; // This is an arbitrary number.
private static final String TAG = "PullNotifications"; // logging tag and Volley request tag
public PullNotificationService() {
@ -62,82 +76,80 @@ public class PullNotificationService extends IntentService {
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
String domain = preferences.getString("domain", null);
String accessToken = preferences.getString("accessToken", null);
long date = preferences.getLong("lastUpdate", 0);
Date lastUpdate = null;
if (date != 0) {
lastUpdate = new Date(date);
}
checkNotifications(domain, accessToken, lastUpdate);
String lastUpdateId = preferences.getString("lastUpdateId", null);
checkNotifications(domain, accessToken, lastUpdateId);
}
private void checkNotifications(final String domain, final String accessToken,
final Date lastUpdate) {
String endpoint = getString(R.string.endpoint_notifications);
String url = "https://" + domain + endpoint;
JsonArrayRequest request = new JsonArrayRequest(url,
new Response.Listener<JSONArray>() {
final String lastUpdateId) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public void onResponse(JSONArray response) {
List<Notification> notifications;
try {
notifications = Notification.parse(response);
} catch (JSONException e) {
onCheckNotificationsFailure(e);
return;
}
onCheckNotificationsSuccess(notifications, lastUpdate);
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder()
.header("Authorization", String.format("Bearer %s", accessToken));
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onCheckNotificationsFailure(error);
}
}) {
})
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
MastodonAPI api = retrofit.create(MastodonAPI.class);
api.notifications(null, lastUpdateId, null).enqueue(new Callback<List<Notification>>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
onCheckNotificationsSuccess(response.body(), lastUpdateId);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<List<Notification>> call, Throwable t) {
onCheckNotificationsFailure((Exception) t);
}
});
}
private void onCheckNotificationsSuccess(List<Notification> notifications, Date lastUpdate) {
Date newest = null;
private void onCheckNotificationsSuccess(List<com.keylesspalace.tusky.entity.Notification> notifications, String lastUpdateId) {
List<MentionResult> mentions = new ArrayList<>();
for (Notification notification : notifications) {
if (notification.getType() == Notification.Type.MENTION) {
Status status = notification.getStatus();
for (com.keylesspalace.tusky.entity.Notification notification : notifications) {
if (notification.type == com.keylesspalace.tusky.entity.Notification.Type.MENTION) {
Status status = notification.status;
if (status != null) {
Date createdAt = status.getCreatedAt();
if (lastUpdate == null || createdAt.after(lastUpdate)) {
MentionResult mention = new MentionResult();
mention.content = status.getContent().toString();
mention.displayName = notification.getDisplayName();
mention.avatarUrl = status.getAvatar();
mentions.add(mention);
}
if (newest == null || createdAt.after(newest)) {
newest = createdAt;
}
MentionResult mention = new MentionResult();
mention.content = status.content.toString();
mention.displayName = notification.account.displayName;
mention.avatarUrl = status.account.avatar;
mentions.add(mention);
}
}
}
long now = new Date().getTime();
if (mentions.size() > 0) {
if (notifications.size() > 0) {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putLong("lastUpdate", now);
editor.putString("lastUpdateId", notifications.get(0).id);
editor.apply();
}
if (mentions.size() > 0) {
loadAvatar(mentions, mentions.get(0).avatarUrl);
} else if (newest != null) {
long hoursAgo = (now - newest.getTime()) / (60 * 60 * 1000);
if (hoursAgo >= 1) {
dismissStaleNotifications();
}
}
}
@ -227,10 +239,4 @@ public class PullNotificationService extends IntentService {
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFY_ID, builder.build());
}
private void dismissStaleNotifications() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFY_ID);
}
}

View File

@ -38,16 +38,22 @@ import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.JsonObjectRequest;
import com.keylesspalace.tusky.entity.Status;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
public class ReportActivity extends BaseActivity {
private static final String TAG = "ReportActivity"; // logging tag and Volley request tag
@ -141,45 +147,22 @@ public class ReportActivity extends BaseActivity {
private void sendReport(final String accountId, final String[] statusIds,
final String comment) {
JSONObject parameters = new JSONObject();
try {
parameters.put("account_id", accountId);
parameters.put("status_ids", makeStringArrayCompat(statusIds));
parameters.put("comment", comment);
} catch (JSONException e) {
Log.e(TAG, "Not all the report parameters have been properly set. " + e.getMessage());
onSendFailure(accountId, statusIds, comment);
return;
}
String endpoint = getString(R.string.endpoint_reports);
String url = "https://" + domain + endpoint;
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
onSendSuccess();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onSendFailure(accountId, statusIds, comment);
}
}) {
mastodonAPI.report(accountId, Arrays.asList(statusIds), comment).enqueue(new Callback<ResponseBody>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
onSendSuccess();
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
onSendFailure(accountId, statusIds, comment);
}
});
}
private void onSendSuccess() {
Toast.makeText(this, getString(R.string.confirmation_reported), Toast.LENGTH_SHORT)
.show();
Snackbar bar = Snackbar.make(anyView, getString(R.string.confirmation_reported), Snackbar.LENGTH_SHORT);
bar.show();
finish();
}
@ -197,46 +180,26 @@ public class ReportActivity extends BaseActivity {
}
private void fetchRecentStatuses(String accountId) {
String endpoint = String.format(getString(R.string.endpoint_statuses), accountId);
String url = "https://" + domain + endpoint;
JsonArrayRequest request = new JsonArrayRequest(url,
new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
List<Status> statusList;
try {
statusList = Status.parse(response);
} catch (JSONException e) {
onFetchStatusesFailure(e);
return;
}
// Add all the statuses except reblogs.
List<ReportAdapter.ReportStatus> itemList = new ArrayList<>();
for (Status status : statusList) {
if (status.getRebloggedByDisplayName() == null) {
ReportAdapter.ReportStatus item = new ReportAdapter.ReportStatus(
status.getId(), status.getContent(), false);
itemList.add(item);
}
}
adapter.addItems(itemList);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFetchStatusesFailure(error);
}
}) {
mastodonAPI.accountStatuses(accountId, null, null, null).enqueue(new Callback<List<Status>>() {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
List<Status> statusList = response.body();
List<ReportAdapter.ReportStatus> itemList = new ArrayList<>();
for (Status status : statusList) {
if (status.reblog != null) {
ReportAdapter.ReportStatus item = new ReportAdapter.ReportStatus(
status.id, status.content, false);
itemList.add(item);
}
}
adapter.addItems(itemList);
}
};
request.setTag(TAG);
VolleySingleton.getInstance(this).addToRequestQueue(request);
@Override
public void onFailure(Call<List<Status>> call, Throwable t) {
onFetchStatusesFailure((Exception) t);
}
});
}
private void onFetchStatusesFailure(Exception exception) {

View File

@ -33,6 +33,8 @@ import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import org.json.JSONObject;
@ -41,6 +43,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
* of that is complicated by how they're coupled with Status and Notification and the corresponding
@ -54,6 +60,7 @@ public class SFragment extends Fragment {
protected String accessToken;
protected String loggedInAccountId;
protected String loggedInUsername;
private MastodonAPI api;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -65,6 +72,7 @@ public class SFragment extends Fragment {
accessToken = preferences.getString("accessToken", null);
loggedInAccountId = preferences.getString("loggedInAccountId", null);
loggedInUsername = preferences.getString("loggedInAccountUsername", null);
api = ((BaseActivity) getActivity()).mastodonAPI;
}
@Override
@ -73,117 +81,119 @@ public class SFragment extends Fragment {
super.onDestroy();
}
protected void sendRequest(
int method, String endpoint, JSONObject parameters,
@Nullable Response.Listener<JSONObject> responseListener,
@Nullable Response.ErrorListener errorListener) {
if (responseListener == null) {
// Use a dummy listener if one wasn't specified so the request can be constructed.
responseListener = new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {}
};
}
if (errorListener == null) {
errorListener = new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, "Request Failed: " + error.getMessage());
}
};
}
String url = "https://" + domain + endpoint;
JsonObjectRequest request = new JsonObjectRequest(
method, url, parameters, responseListener, errorListener) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
}
};
request.setTag(TAG);
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
}
protected void postRequest(String endpoint) {
sendRequest(Request.Method.POST, endpoint, null, null, null);
}
protected void reply(Status status) {
String inReplyToId = status.getId();
Status.Mention[] mentions = status.getMentions();
String inReplyToId = status.getActionableId();
Status.Mention[] mentions = status.mentions;
List<String> mentionedUsernames = new ArrayList<>();
for (Status.Mention mention : mentions) {
mentionedUsernames.add(mention.getUsername());
mentionedUsernames.add(mention.username);
}
mentionedUsernames.add(status.getUsername());
mentionedUsernames.add(status.account.username);
mentionedUsernames.remove(loggedInUsername);
Intent intent = new Intent(getContext(), ComposeActivity.class);
intent.putExtra("in_reply_to_id", inReplyToId);
intent.putExtra("reply_visibility", status.getVisibility().toString().toLowerCase());
intent.putExtra("reply_visibility", status.visibility.toString().toLowerCase());
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
startActivity(intent);
}
protected void reblog(final Status status, final boolean reblog,
final RecyclerView.Adapter adapter, final int position) {
String id = status.getId();
String endpoint;
String id = status.getActionableId();
Callback<Status> cb = new Callback<Status>() {
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
status.reblogged = reblog;
adapter.notifyItemChanged(position);
}
@Override
public void onFailure(Call<Status> call, Throwable t) {
}
};
if (reblog) {
endpoint = String.format(getString(R.string.endpoint_reblog), id);
api.reblogStatus(id).enqueue(cb);
} else {
endpoint = String.format(getString(R.string.endpoint_unreblog), id);
api.unreblogStatus(id).enqueue(cb);
}
sendRequest(Request.Method.POST, endpoint, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
status.setReblogged(reblog);
adapter.notifyItemChanged(position);
}
}, null);
}
protected void favourite(final Status status, final boolean favourite,
final RecyclerView.Adapter adapter, final int position) {
String id = status.getId();
String endpoint;
if (favourite) {
endpoint = String.format(getString(R.string.endpoint_favourite), id);
} else {
endpoint = String.format(getString(R.string.endpoint_unfavourite), id);
}
sendRequest(Request.Method.POST, endpoint, null, new Response.Listener<JSONObject>() {
String id = status.getActionableId();
Callback<Status> cb = new Callback<Status>() {
@Override
public void onResponse(JSONObject response) {
status.setFavourited(favourite);
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
status.favourited = favourite;
adapter.notifyItemChanged(position);
}
}, null);
@Override
public void onFailure(Call<Status> call, Throwable t) {
}
};
if (favourite) {
api.favouriteStatus(id).enqueue(cb);
} else {
api.unfavouriteStatus(id).enqueue(cb);
}
}
protected void follow(String id) {
String endpoint = String.format(getString(R.string.endpoint_follow), id);
postRequest(endpoint);
api.followAccount(id).enqueue(new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
}
});
}
private void block(String id) {
String endpoint = String.format(getString(R.string.endpoint_block), id);
postRequest(endpoint);
api.blockAccount(id).enqueue(new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
}
});
}
private void delete(String id) {
String endpoint = String.format(getString(R.string.endpoint_delete), id);
sendRequest(Request.Method.DELETE, endpoint, null, null, null);
api.deleteStatus(id).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
protected void more(Status status, View view, final AdapterItemRemover adapter,
final int position) {
final String id = status.getId();
final String accountId = status.getAccountId();
final String accountUsename = status.getUsername();
final Spanned content = status.getContent();
final String id = status.getActionableId();
final String accountId = status.getActionableStatus().account.id;
final String accountUsename = status.getActionableStatus().account.username;
final Spanned content = status.getActionableStatus().content;
final String statusUrl = status.getActionableStatus().url;
PopupMenu popup = new PopupMenu(getContext(), view);
// Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) {
@ -196,8 +206,12 @@ public class SFragment extends Fragment {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.status_follow: {
follow(accountId);
case R.id.status_share: {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_to)));
return true;
}
case R.id.status_block: {
@ -234,19 +248,17 @@ public class SFragment extends Fragment {
protected void viewMedia(String url, Status.MediaAttachment.Type type) {
switch (type) {
case IMAGE: {
Fragment newFragment;
if (fileExtensionMatches(url, "gif")) {
newFragment = ViewGifFragment.newInstance(url);
} else {
newFragment = ViewMediaFragment.newInstance(url);
}
Fragment newFragment = ViewMediaFragment.newInstance(url);
FragmentManager manager = getFragmentManager();
manager.beginTransaction()
.setCustomAnimations(R.anim.zoom_in, R.anim.zoom_out, R.anim.zoom_in, R.anim.zoom_out)
.add(R.id.overlay_fragment_container, newFragment)
.addToBackStack(null)
.commit();
break;
}
case GIFV:
case VIDEO: {
Intent intent = new Intent(getContext(), ViewVideoActivity.class);
intent.putExtra("url", url);
@ -264,7 +276,7 @@ public class SFragment extends Fragment {
protected void viewThread(Status status) {
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
intent.putExtra("id", status.getId());
intent.putExtra("id", status.id);
startActivity(intent);
}

View File

@ -0,0 +1,18 @@
package com.keylesspalace.tusky;
import android.text.Spanned;
import com.emojione.Emojione;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
@Override
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false));
}
}

View File

@ -1,357 +0,0 @@
/* 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.annotation.Nullable;
import android.text.Spanned;
import com.emojione.Emojione;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class Status {
enum Visibility {
PUBLIC,
UNLISTED,
PRIVATE,
}
private String id;
private String accountId;
private String displayName;
/** the username with the remote domain appended, like @domain.name, if it's a remote account */
private String username;
/** the main text of the status, marked up with style for links & mentions, etc */
private Spanned content;
/** the fully-qualified url of the avatar image */
private String avatar;
private String rebloggedByDisplayName;
/** when the status was initially created */
private Date createdAt;
/** whether the authenticated user has reblogged this status */
private boolean reblogged;
/** whether the authenticated user has favourited this status */
private boolean favourited;
private boolean sensitive;
private String spoilerText;
private Visibility visibility;
private MediaAttachment[] attachments;
private Mention[] mentions;
static final int MAX_MEDIA_ATTACHMENTS = 4;
public Status(String id, String accountId, String displayName, String username, Spanned content,
String avatar, Date createdAt, boolean reblogged, boolean favourited,
String visibility) {
this.id = id;
this.accountId = accountId;
this.displayName = displayName;
this.username = username;
this.content = content;
this.avatar = avatar;
this.createdAt = createdAt;
this.reblogged = reblogged;
this.favourited = favourited;
this.spoilerText = "";
this.visibility = Visibility.valueOf(visibility.toUpperCase());
this.attachments = new MediaAttachment[0];
this.mentions = new Mention[0];
}
String getId() {
return id;
}
String getAccountId() {
return accountId;
}
String getDisplayName() {
return displayName;
}
String getUsername() {
return username;
}
Spanned getContent() {
return content;
}
String getAvatar() {
return avatar;
}
Date getCreatedAt() {
return createdAt;
}
String getRebloggedByDisplayName() {
return rebloggedByDisplayName;
}
boolean getReblogged() {
return reblogged;
}
boolean getFavourited() {
return favourited;
}
boolean getSensitive() {
return sensitive;
}
String getSpoilerText() {
return spoilerText;
}
Visibility getVisibility() {
return visibility;
}
MediaAttachment[] getAttachments() {
return attachments;
}
Mention[] getMentions() {
return mentions;
}
private void setRebloggedByDisplayName(String name) {
rebloggedByDisplayName = name;
}
void setReblogged(boolean reblogged) {
this.reblogged = reblogged;
}
void setFavourited(boolean favourited) {
this.favourited = favourited;
}
private void setSpoilerText(String spoilerText) {
this.spoilerText = spoilerText;
}
private void setMentions(Mention[] mentions) {
this.mentions = mentions;
}
private void setAttachments(MediaAttachment[] attachments, boolean sensitive) {
this.attachments = attachments;
this.sensitive = sensitive;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Status)) {
return false;
}
Status status = (Status) other;
return status.id.equals(this.id);
}
@SuppressWarnings("SimpleDateFormat") // UTC needs to not specify a Locale
@Nullable
private static Date parseDate(String dateTime) {
Date date;
String s = dateTime.replace("Z", "+00:00");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
try {
date = format.parse(s);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
return date;
}
private static MediaAttachment.Type parseMediaType(@Nullable String type) {
if (type == null) {
return MediaAttachment.Type.UNKNOWN;
}
switch (type.toUpperCase()) {
case "IMAGE": return MediaAttachment.Type.IMAGE;
case "GIFV":
case "VIDEO": return MediaAttachment.Type.VIDEO;
default: return MediaAttachment.Type.UNKNOWN;
}
}
public static Status parse(JSONObject object, boolean isReblog) throws JSONException {
String id = object.getString("id");
String content = object.getString("content");
Date createdAt = parseDate(object.getString("created_at"));
boolean reblogged = object.optBoolean("reblogged");
boolean favourited = object.optBoolean("favourited");
String spoilerText = object.getString("spoiler_text");
boolean sensitive = object.optBoolean("sensitive");
String visibility = object.getString("visibility");
JSONObject account = object.getJSONObject("account");
String accountId = account.getString("id");
String displayName = account.getString("display_name");
if (displayName.isEmpty()) {
displayName = account.getString("username");
}
String username = account.getString("acct");
String avatarUrl = account.getString("avatar");
String avatar;
if (!avatarUrl.equals("/avatars/original/missing.png")) {
avatar = avatarUrl;
} else {
avatar = "";
}
JSONArray mentionsArray = object.getJSONArray("mentions");
Mention[] mentions = null;
if (mentionsArray != null) {
int n = mentionsArray.length();
mentions = new Mention[n];
for (int i = 0; i < n; i++) {
JSONObject mention = mentionsArray.getJSONObject(i);
String url = mention.getString("url");
String mentionedUsername = mention.getString("acct");
String mentionedAccountId = mention.getString("id");
mentions[i] = new Mention(url, mentionedUsername, mentionedAccountId);
}
}
JSONArray mediaAttachments = object.getJSONArray("media_attachments");
MediaAttachment[] attachments = null;
if (mediaAttachments != null) {
int n = mediaAttachments.length();
attachments = new MediaAttachment[n];
for (int i = 0; i < n; i++) {
JSONObject attachment = mediaAttachments.getJSONObject(i);
String url = attachment.getString("url");
String previewUrl = attachment.getString("preview_url");
String type = attachment.getString("type");
attachments[i] = new MediaAttachment(url, previewUrl, parseMediaType(type));
}
}
Status reblog = null;
/* This case shouldn't be hit after the first recursion at all. But if this method is
* passed unusual data this check will prevent extra recursion */
if (!isReblog) {
JSONObject reblogObject = object.optJSONObject("reblog");
if (reblogObject != null) {
reblog = parse(reblogObject, true);
}
}
Status status;
if (reblog != null) {
status = reblog;
status.setRebloggedByDisplayName(displayName);
} else {
Spanned contentPlus = HtmlUtils.fromHtml(Emojione.shortnameToUnicode(content, false));
status = new Status(
id, accountId, displayName, username, contentPlus, avatar, createdAt,
reblogged, favourited, visibility);
if (mentions != null) {
status.setMentions(mentions);
}
if (attachments != null) {
status.setAttachments(attachments, sensitive);
}
if (!spoilerText.isEmpty()) {
status.setSpoilerText(spoilerText);
}
}
return status;
}
public static List<Status> parse(JSONArray array) throws JSONException {
List<Status> statuses = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
JSONObject object = array.getJSONObject(i);
statuses.add(parse(object, false));
}
return statuses;
}
static class MediaAttachment {
enum Type {
IMAGE,
VIDEO,
UNKNOWN,
}
private String url;
private String previewUrl;
private Type type;
MediaAttachment(String url, String previewUrl, Type type) {
this.url = url;
this.previewUrl = previewUrl;
this.type = type;
}
String getUrl() {
return url;
}
String getPreviewUrl() {
return previewUrl;
}
Type getType() {
return type;
}
}
static class Mention {
private String url;
private String username;
private String id;
Mention(String url, String username, String id) {
this.url = url;
this.username = username;
this.id = id;
}
String getUrl() {
return url;
}
String getUsername() {
return username;
}
String getId() {
return id;
}
}
}

View File

@ -17,6 +17,8 @@ package com.keylesspalace.tusky;
import android.view.View;
import com.keylesspalace.tusky.entity.Status;
interface StatusActionListener {
void onReply(int position);
void onReblog(final boolean reblog, final int position);

View File

@ -30,6 +30,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import com.keylesspalace.tusky.entity.Status;
import com.squareup.picasso.Picasso;
import com.varunest.sparkbutton.SparkButton;
import com.varunest.sparkbutton.SparkEventListener;
@ -124,8 +125,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
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 (mention.username.equals(accountUsername)) {
id = mention.id;
}
}
if (id != null) {
@ -227,7 +228,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
for (int i = 0; i < n; i++) {
String previewUrl = attachments[i].getPreviewUrl();
String previewUrl = attachments[i].previewUrl;
previews[i].setVisibility(View.VISIBLE);
@ -236,8 +237,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
.placeholder(mediaPreviewUnloadedId)
.into(previews[i]);
final String url = attachments[i].getUrl();
final Status.MediaAttachment.Type type = attachments[i].getType();
final String url = attachments[i].url;
final Status.MediaAttachment.Type type = attachments[i].type;
previews[i].setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -339,33 +340,35 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
}
void setupWithStatus(Status status, StatusActionListener listener) {
setDisplayName(status.getDisplayName());
setUsername(status.getUsername());
setCreatedAt(status.getCreatedAt());
setContent(status.getContent(), status.getMentions(), listener);
setAvatar(status.getAvatar());
setReblogged(status.getReblogged());
setFavourited(status.getFavourited());
String rebloggedByDisplayName = status.getRebloggedByDisplayName();
if (rebloggedByDisplayName == null) {
Status realStatus = status.getActionableStatus();
setDisplayName(realStatus.account.displayName);
setUsername(realStatus.account.username);
setCreatedAt(realStatus.createdAt);
setContent(realStatus.content, realStatus.mentions, listener);
setAvatar(realStatus.account.avatar);
setReblogged(realStatus.reblogged);
setFavourited(realStatus.favourited);
String rebloggedByDisplayName = status.account.displayName;
if (status.reblog == null) {
hideRebloggedByDisplayName();
} else {
setRebloggedByDisplayName(rebloggedByDisplayName);
}
Status.MediaAttachment[] attachments = status.getAttachments();
boolean sensitive = status.getSensitive();
Status.MediaAttachment[] attachments = realStatus.attachments;
boolean sensitive = realStatus.sensitive;
setMediaPreviews(attachments, sensitive, listener);
/* A status without attachments is sometimes still marked sensitive, so it's necessary to
* check both whether there are any attachments and if it's marked sensitive. */
if (!sensitive || attachments.length == 0) {
hideSensitiveMediaWarning();
}
setupButtons(listener, status.getAccountId());
setRebloggingEnabled(status.getVisibility() != Status.Visibility.PRIVATE);
if (status.getSpoilerText().isEmpty()) {
setupButtons(listener, realStatus.account.id);
setRebloggingEnabled(realStatus.visibility != Status.Visibility.PRIVATE);
if (realStatus.spoilerText.isEmpty()) {
hideSpoilerText();
} else {
setSpoilerText(status.getSpoilerText());
setSpoilerText(realStatus.spoilerText);
}
}
}

View File

@ -20,6 +20,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.entity.Status;
import java.util.ArrayList;
import java.util.List;

View File

@ -21,6 +21,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.entity.Status;
import java.util.ArrayList;
import java.util.List;

View File

@ -31,6 +31,7 @@ import com.android.volley.AuthFailureError;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.keylesspalace.tusky.entity.Status;
import org.json.JSONArray;
import org.json.JSONException;
@ -39,6 +40,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
private static final String TAG = "Timeline"; // logging tag and Volley request tag
@ -117,7 +121,7 @@ public class TimelineFragment extends SFragment implements
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
Status status = adapter.getItem(adapter.getItemCount() - 2);
if (status != null) {
sendFetchTimelineRequest(status.getId());
sendFetchTimelineRequest(status.id);
} else {
sendFetchTimelineRequest();
}
@ -168,67 +172,43 @@ public class TimelineFragment extends SFragment implements
}
private void sendFetchTimelineRequest(final String fromId) {
String endpoint;
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
Callback<List<Status>> cb = new Callback<List<Status>>() {
@Override
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
onFetchTimelineSuccess(response.body(), fromId);
}
@Override
public void onFailure(Call<List<Status>> call, Throwable t) {
onFetchTimelineFailure((Exception) t);
}
};
switch (kind) {
default:
case HOME: {
endpoint = getString(R.string.endpoint_timelines_home);
break;
}
case MENTIONS: {
endpoint = getString(R.string.endpoint_timelines_mentions);
api.homeTimeline(fromId, null, null).enqueue(cb);
break;
}
case PUBLIC: {
endpoint = getString(R.string.endpoint_timelines_public);
api.publicTimeline(null, fromId, null, null).enqueue(cb);
break;
}
case TAG: {
endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtagOrId);
api.hashtagTimeline(hashtagOrId, null, fromId, null, null).enqueue(cb);
break;
}
case USER: {
endpoint = String.format(getString(R.string.endpoint_statuses), hashtagOrId);
api.accountStatuses(hashtagOrId, fromId, null, null).enqueue(cb);
break;
}
case FAVOURITES: {
endpoint = getString(R.string.endpoint_favourites);
api.favourites(fromId, null, null).enqueue(cb);
break;
}
}
String url = "https://" + domain + endpoint;
if (fromId != null) {
url += "?max_id=" + fromId;
}
JsonArrayRequest request = new JsonArrayRequest(url,
new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
List<Status> statuses = null;
try {
statuses = Status.parse(response);
} catch (JSONException e) {
onFetchTimelineFailure(e);
}
if (statuses != null) {
onFetchTimelineSuccess(statuses, fromId);
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onFetchTimelineFailure(error);
}
}) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
}
};
request.setTag(TAG);
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
}
private void sendFetchTimelineRequest() {
@ -237,7 +217,7 @@ public class TimelineFragment extends SFragment implements
private static boolean findStatus(List<Status> statuses, String id) {
for (Status status : statuses) {
if (status.getId().equals(id)) {
if (status.id.equals(id)) {
return true;
}
}
@ -281,7 +261,7 @@ public class TimelineFragment extends SFragment implements
public void onLoadMore() {
Status status = adapter.getItem(adapter.getItemCount() - 2);
if (status != null) {
sendFetchTimelineRequest(status.getId());
sendFetchTimelineRequest(status.id);
} else {
sendFetchTimelineRequest();
}

View File

@ -1,58 +0,0 @@
/* 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;
import android.webkit.WebView;
public class ViewGifFragment extends Fragment {
public static ViewGifFragment newInstance(String url) {
Bundle arguments = new Bundle();
ViewGifFragment fragment = new ViewGifFragment();
arguments.putString("url", url);
fragment.setArguments(arguments);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_view_gif, container, false);
String url = getArguments().getString("url");
WebView gifView = (WebView) rootView.findViewById(R.id.gif_view);
gifView.loadUrl(url);
rootView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
return rootView;
}
private void dismiss() {
getFragmentManager().popBackStack();
}
}

View File

@ -21,8 +21,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import uk.co.senab.photoview.PhotoView;
import uk.co.senab.photoview.PhotoViewAttacher;
public class ViewMediaFragment extends Fragment {
public static ViewMediaFragment newInstance(String url) {
@ -40,17 +43,36 @@ public class ViewMediaFragment extends Fragment {
Bundle arguments = getArguments();
String url = arguments.getString("url");
NetworkImageView image = (NetworkImageView) rootView.findViewById(R.id.view_media_image);
ImageLoader imageLoader = VolleySingleton.getInstance(getContext()).getImageLoader();
image.setImageUrl(url, imageLoader);
PhotoView photoView = (PhotoView) rootView.findViewById(R.id.view_media_image);
rootView.setOnClickListener(new View.OnClickListener() {
final PhotoViewAttacher attacher = new PhotoViewAttacher(photoView);
attacher.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() {
@Override
public void onClick(View v) {
public void onPhotoTap(View view, float x, float y) {
}
@Override
public void onOutsidePhotoTap() {
dismiss();
}
});
Picasso.with(getContext())
.load(url)
.into(photoView, new Callback() {
@Override
public void onSuccess() {
attacher.update();
}
@Override
public void onError() {
}
});
return rootView;
}

View File

@ -30,12 +30,17 @@ import android.view.ViewGroup;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
public class ViewThreadFragment extends SFragment implements StatusActionListener {
private RecyclerView recyclerView;
private ThreadAdapter adapter;
@ -78,54 +83,39 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
}
private void sendStatusRequest(final String id) {
String endpoint = String.format(getString(R.string.endpoint_get_status), id);
super.sendRequest(Request.Method.GET, endpoint, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Status status;
try {
status = Status.parse(response, false);
} catch (JSONException e) {
onThreadRequestFailure(id);
return;
}
int position = adapter.insertStatus(status);
recyclerView.scrollToPosition(position);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onThreadRequestFailure(id);
}
});
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
api.status(id).enqueue(new Callback<Status>() {
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
int position = adapter.insertStatus(response.body());
recyclerView.scrollToPosition(position);
}
@Override
public void onFailure(Call<Status> call, Throwable t) {
onThreadRequestFailure(id);
}
});
}
private void sendThreadRequest(final String id) {
String endpoint = String.format(getString(R.string.endpoint_context), id);
super.sendRequest(Request.Method.GET, endpoint, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
List<Status> ancestors =
Status.parse(response.getJSONArray("ancestors"));
List<Status> descendants =
Status.parse(response.getJSONArray("descendants"));
adapter.addAncestors(ancestors);
adapter.addDescendants(descendants);
} catch (JSONException e) {
onThreadRequestFailure(id);
}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onThreadRequestFailure(id);
}
});
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
api.statusContext(id).enqueue(new Callback<StatusContext>() {
@Override
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
StatusContext context = response.body();
adapter.addAncestors(context.ancestors);
adapter.addDescendants(context.descendants);
}
@Override
public void onFailure(Call<StatusContext> call, Throwable t) {
onThreadRequestFailure(id);
}
});
}
private void onThreadRequestFailure(final String id) {
@ -162,7 +152,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
public void onViewThread(int position) {
Status status = adapter.getItem(position);
if (thisThreadsStatusId.equals(status.getId())) {
if (thisThreadsStatusId.equals(status.id)) {
// If already viewing this thread, don't reopen it.
return;
}

View File

@ -0,0 +1,65 @@
/* 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.entity;
import android.text.Spanned;
import com.google.gson.annotations.SerializedName;
public class Account {
public String id;
@SerializedName("acct")
public String username;
@SerializedName("display_name")
public String displayName;
public Spanned note;
public String url;
public String avatar;
public String header;
public boolean locked;
@SerializedName("followers_count")
public String followersCount;
@SerializedName("following_count")
public String followingCount;
@SerializedName("statuses_count")
public String statusesCount;
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Account)) {
return false;
}
Account account = (Account) other;
return account.id.equals(this.id);
}
}

View File

@ -0,0 +1,17 @@
package com.keylesspalace.tusky.entity;
import com.google.gson.annotations.SerializedName;
public class Media {
public String id;
public String type;
public String url;
@SerializedName("preview_url")
public String previewUrl;
@SerializedName("text_url")
public String textUrl;
}

View File

@ -0,0 +1,55 @@
/* 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.entity;
import com.google.gson.annotations.SerializedName;
public class Notification {
public enum Type {
@SerializedName("mention")
MENTION,
@SerializedName("reblog")
REBLOG,
@SerializedName("favourite")
FAVOURITE,
@SerializedName("follow")
FOLLOW,
}
public Type type;
public String id;
public Account account;
public Status status;
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Notification)) {
return false;
}
Notification notification = (Notification) other;
return notification.id.equals(this.id);
}
}

View File

@ -0,0 +1,18 @@
package com.keylesspalace.tusky.entity;
import com.google.gson.annotations.SerializedName;
public class Relationship {
public String id;
public boolean following;
@SerializedName("followed_by")
public boolean followedBy;
public boolean blocking;
public boolean muting;
public boolean requested;
}

View File

@ -0,0 +1,136 @@
/* 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.entity;
import android.text.Spanned;
import com.google.gson.annotations.SerializedName;
import java.util.Date;
public class Status {
private Status actionableStatus;
public String url;
@SerializedName("reblogs_count")
public String reblogsCount;
@SerializedName("favourites_count")
public String favouritesCount;
@SerializedName("in_reply_to_id")
public String inReplyToId;
@SerializedName("in_reply_to_account_id")
public String inReplyToAccountId;
public String getActionableId() {
return reblog == null ? id : reblog.id;
}
public Status getActionableStatus() {
return reblog == null ? this : reblog;
}
public enum Visibility {
@SerializedName("public")
PUBLIC,
@SerializedName("unlisted")
UNLISTED,
@SerializedName("private")
PRIVATE,
}
public String id;
public Account account;
public Spanned content;
public Status reblog;
@SerializedName("created_at")
public Date createdAt;
public boolean reblogged;
public boolean favourited;
public boolean sensitive;
@SerializedName("spoiler_text")
public String spoilerText;
public Visibility visibility;
@SerializedName("media_attachments")
public MediaAttachment[] attachments;
public Mention[] mentions;
public static final int MAX_MEDIA_ATTACHMENTS = 4;
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object other) {
if (this.id == null) {
return this == other;
} else if (!(other instanceof Status)) {
return false;
}
Status status = (Status) other;
return status.id.equals(this.id);
}
public static class MediaAttachment {
public enum Type {
@SerializedName("image")
IMAGE,
@SerializedName("gifv")
GIFV,
@SerializedName("video")
VIDEO,
UNKNOWN,
}
public String url;
@SerializedName("preview_url")
public String previewUrl;
@SerializedName("text_url")
public String textUrl;
@SerializedName("remote_url")
public String remoteUrl;
public Type type;
}
public static class Mention {
public String id;
public String url;
@SerializedName("acct")
public String username;
}
}

View File

@ -0,0 +1,8 @@
package com.keylesspalace.tusky.entity;
import java.util.List;
public class StatusContext {
public List<Status> ancestors;
public List<Status> descendants;
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:interpolator="@android:anim/linear_interpolator"
android:fromXScale=".1"
android:toXScale="1"
android:fromYScale=".1"
android:toYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:duration="200"
android:fillAfter="true">
</scale>
<alpha
android:interpolator="@android:anim/linear_interpolator"
android:fromAlpha="0"
android:toAlpha="1"
android:duration="300" />
</set>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:interpolator="@android:anim/linear_interpolator"
android:fromXScale="1"
android:toXScale=".1"
android:fromYScale="1"
android:toYScale=".1"
android:pivotX="50%"
android:pivotY="50%"
android:duration="200"
android:fillAfter="true">
</scale>
<alpha
android:interpolator="@android:anim/linear_interpolator"
android:fromAlpha="1"
android:toAlpha="0"
android:duration="300" />
</set>

View File

@ -2,7 +2,6 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="700px" android:height="335px" />
<solid android:color="@color/color_background_dark" />
</shape>

View File

@ -1,7 +0,0 @@
<vector android:height="24dp" android:viewportHeight="850.3937"
android:viewportWidth="850.3937" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m410.4,48.8c-9.1,0 -18.1,3.5 -25.1,10.4L84.7,359.9c-1.6,1.6 -2.9,3.2 -4.1,5 -6,6.3 -9.7,14.9 -9.7,24.4l0,70.9c0,11.3 5.2,21.3 13.4,27.8 1.6,3.2 3.8,6.2 6.5,8.9L391.5,797.5c13.9,13.9 36.2,13.9 50.1,0l50.1,-50.1c13.9,-13.9 13.9,-36.2 0,-50.1l-201.7,-201.7 454.1,0c19.6,0 35.4,-15.8 35.4,-35.4l0,-70.9c0,-19.6 -15.8,-35.4 -35.4,-35.4l-452.9,0 194.4,-194.4c13.9,-13.9 13.9,-36.2 0,-50.1l-50.1,-50.1c-6.9,-6.9 -16,-10.4 -25.1,-10.4z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="10.62992096"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
</vector>

View File

@ -1,11 +0,0 @@
<vector android:height="24dp" android:viewportHeight="708.66144"
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m86.4,17.7c-37.3,0 -67.3,30 -67.3,67.3l0,538.6c0,37.3 30,67.3 67.3,67.3l537.2,0c37.3,0 67.3,-30 67.3,-67.3l0,-462.4c-17.6,17.9 -35.4,35.6 -53.2,53.3l0,352.4c0,39.3 -31.6,70.9 -70.9,70.9l-425.2,0c-39.3,0 -70.9,-31.6 -70.9,-70.9l0,-425.2c0,-39.3 31.6,-70.9 70.9,-70.9l358.9,0c18,-17.7 36,-35.3 53.8,-53.2l-468,0z"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="33.62945938"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m672.8,8.2 l25.1,25.1c13.9,13.9 13.9,36.2 0,50.1L361.6,420.4C347.1,434.2 199.5,537.2 185.6,523.3l-0.8,-0.8C170.9,508.6 272.1,359.2 286.6,344.7L622.7,8.2c13.9,-13.9 36.2,-13.9 50.1,0z"
android:strokeAlpha="1" android:strokeColor="#cccccc"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="24dp" android:viewportHeight="42.519684"
android:viewportWidth="42.519684" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M7.81,16.57C7.36,16.55 6.87,16.73 6.43,17.17C6.27,17.34 6.13,17.51 5.99,17.68C5.86,17.79 5.73,17.91 5.63,18.04C5.49,18.21 5.4,18.42 5.29,18.62C5.29,18.62 5.29,18.63 5.28,18.63C5.04,19 4.81,19.37 4.6,19.76C4.1,20.87 3.96,22.05 4.07,23.25C4.23,24.34 4.77,25.2 5.58,25.93C6.51,26.7 7.61,26.67 8.71,26.34C9.63,26.03 10.41,25.46 11.14,24.84C11.78,24.32 12.3,23.72 12.71,23.01C13.13,22.11 13.19,21.16 13.02,20.2C12.92,19.29 12.36,18.64 11.77,18.01C11.12,17.34 10.29,17.03 9.38,16.89C9.15,16.86 8.91,16.84 8.68,16.85C8.42,16.68 8.13,16.58 7.81,16.57zM22.36,16.9C21.94,16.96 21.57,17.11 21.23,17.31C20.89,17.38 20.54,17.55 20.21,17.88C19.3,18.75 18.74,19.9 18.28,21.05C17.88,22.29 17.95,23.52 18.34,24.73C18.78,25.83 19.59,26.53 20.69,26.93C21.86,27.26 22.88,26.85 23.82,26.15C24.66,25.41 25.2,24.46 25.56,23.41C25.93,22.47 26.06,21.49 26.01,20.48C25.99,19.54 25.59,18.73 25.04,17.99C24.3,17.13 23.45,16.9 22.36,16.9zM35.14,17.19C34.99,17.2 34.86,17.24 34.73,17.28C34.26,17.25 33.75,17.42 33.29,17.88C32.46,18.61 31.87,19.5 31.43,20.52C31.01,21.82 31.15,23.13 31.64,24.39C32.06,25.39 32.78,26.08 33.75,26.53C35.12,26.87 35.98,26.36 36.91,25.4C37.5,24.76 37.97,24.04 38.36,23.26C38.82,22.34 38.9,21.37 38.77,20.37C38.61,19.46 38.13,18.74 37.49,18.11C37.03,17.63 36.44,17.38 35.81,17.23C35.56,17.19 35.34,17.18 35.14,17.19zM8.68,21.7C8.7,21.72 8.72,21.73 8.74,21.75C8.63,21.69 8.89,22.06 8.69,21.73C8.68,21.72 8.68,21.71 8.68,21.7zM35.02,23.23C35.03,23.23 35.03,23.24 35.04,23.24C35.28,23.41 35.15,23.37 35.02,23.23z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="24dp" android:viewportHeight="42.519684"
android:viewportWidth="42.519684" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M21.68,5.81C20.21,5.78 17.68,14.98 16.48,15.81C15.27,16.65 5.77,15.82 5.29,17.2C4.81,18.59 12.77,23.84 13.2,25.24C13.62,26.64 9.89,35.42 11.06,36.3C12.23,37.19 19.68,31.24 21.14,31.27C22.61,31.3 29.81,37.56 31.01,36.72C32.21,35.88 28.86,26.96 29.34,25.57C29.82,24.19 37.99,19.28 37.57,17.88C37.15,16.47 27.62,16.91 26.45,16.02C25.29,15.14 23.14,5.84 21.68,5.81z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="16dp" android:viewportHeight="566.92914"
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.6,24.8C287.6,24.9 291.2,27.1 293,30.7L363.4,173.4L520.9,196.3C529.6,197.6 533.1,208.3 526.8,214.4L412.8,325.5L439.7,482.3C441.2,491 432.1,497.6 424.3,493.5L283.5,419.5L142.6,493.5C134.8,497.6 125.7,491 127.2,482.3L154.1,325.5L40.2,214.4C33.8,208.3 37.3,197.6 46,196.3L203.5,173.4L273.9,30.7C275.7,27.1 279.5,24.8 283.6,24.8z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="16dp" android:viewportHeight="566.92914"
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.5,70.9A88.6,88.6 0,0 1,372 159.4A88.6,88.6 0,0 1,283.5 248A88.6,88.6 0,0 1,194.9 159.4A88.6,88.6 0,0 1,283.5 70.9zM194.9,311.3C194.9,311.3 229.1,336.6 283.5,336.6C338.4,336.6 370.5,311.3 370.5,311.3C496.1,407.5 460.6,478.3 460.6,478.3L106.3,478.3C106.3,478.3 70.9,407.5 194.9,311.3z"
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>

View File

@ -1,31 +0,0 @@
<vector android:height="48dp" android:viewportHeight="1133.894"
android:viewportWidth="1134.6519" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="M52.7,262.2L1081.9,262.2A38.6,38.6 0,0 1,1120.5 300.8L1120.5,833.1A38.6,38.6 0,0 1,1081.9 871.7L52.7,871.7A38.6,38.6 0,0 1,14.2 833.1L14.2,300.8A38.6,38.6 0,0 1,52.7 262.2z"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="28.34645653"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m19.6,458.3c104.2,-9.7 76.2,61 365.2,125.3 61.9,13.8 50,40.6 96.2,58 105.8,39.9 376.7,15.8 639.8,33.5"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m1011.8,494c0,0 -130.5,-8 -158.9,-39.2 -142.4,-156.4 -193.3,0.9 -217,-9.7 -74.7,-33.3 -65,21.3 -199.8,103.2 -20.5,12.4 -8.8,16.9 39.1,18.1 143.3,3.8 -74.6,16.2 24.6,18.4l115.8,2.6"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m1121.2,496.7c0,0 -254.5,-33.7 -505.7,90.3 -98.7,48.8 350.1,80.7 350.1,80.7"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
<path android:fillColor="#00000000"
android:pathData="m459,531.9 l-245.5,-0" android:strokeAlpha="1"
android:strokeColor="#ffffff" android:strokeLineCap="butt"
android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
<path android:fillColor="#00000000"
android:pathData="M14.2,639C390.1,602 473.1,743.8 1118.5,752.1"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="35.43307114"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="M277.5,425.5m-62.9,0a62.9,62.9 0,1 1,125.7 0a62.9,62.9 0,1 1,-125.7 0"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307114"/>
</vector>

View File

@ -1,11 +0,0 @@
<vector android:height="48dp" android:viewportHeight="1133.894"
android:viewportWidth="1134.6519" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#9d9d9d"
android:pathData="m277.5,344.9c-44.3,0 -80.6,36.3 -80.6,80.6 -0,44.3 36.3,80.6 80.6,80.6 44.3,0 80.6,-36.3 80.6,-80.6 -0,-44.3 -36.3,-80.6 -80.6,-80.6zM277.5,380.3c25.1,0 45.1,20 45.1,45.1 0,25.1 -20,45.1 -45.1,45.1 -25.1,0 -45.1,-20 -45.1,-45.1 0,-25.1 20,-45.1 45.1,-45.1zM135.7,615.2c-38,-0.1 -78.6,1.8 -123.2,6.2l-0.1,35.3 306.3,31.4 2.1,-20.4c4.8,0.8 9.6,1.5 14.4,2.4l6.3,-34.9c-65,-11.8 -130.2,-19.8 -205.8,-20zM549.6,676.1 L549.2,679.4 542.6,712.8c135.7,27.1 307.8,53.6 575.7,57.1l2,-35.3 -570.7,-58.5zM213.5,517.8 L213.5,546.1 459,546.1 459,517.8 213.5,517.8zM1034.5,478.4c-83.3,0.3 -215.7,11.2 -354.9,65.1l10.2,26.4c134.7,-52.1 263.8,-62.9 344.7,-63.2 53,-0.2 84.7,4.1 84.7,4.1l3.7,-28.1c0,0 -34,-4.5 -88.6,-4.3zM628.4,605.1 L618,631.5c43.4,17 128.1,28.6 204.3,37.2 76.1,8.6 142.3,13.3 142.3,13.3l2,-28.3c0,0 -65.7,-4.7 -141.1,-13.2 -75.4,-8.5 -161.9,-21.6 -197.1,-35.4zM828.5,411.3 L810.2,432.9c10,8.5 20.8,18.8 32.2,31.4 10.5,11.6 25.7,17.8 43,23.3 14.5,4.6 30.4,7.9 46.1,10.7l-269,63.5 6.5,27.6 346,-81.6 -2.4,-27.9c0,0 -32,-2 -67.4,-7.7 -17.7,-2.9 -36.1,-6.7 -51.3,-11.5 -15.2,-4.8 -26.9,-11.3 -30.6,-15.3 -12.2,-13.4 -23.8,-24.6 -34.9,-34zM445.8,525.5c-5.4,3.5 -11.1,7.1 -17,10.7 -5.8,3.5 -10.5,5.8 -14.4,13.3 -2,3.8 -2.8,10.6 -0.3,15.7 2.5,5.1 6.3,7.4 9.3,8.8l9.1,4.5 31.1,-31.1 -17.7,-21.9zM37.7,443.2c-6.1,-0 -12.5,0.3 -19.4,1l2.6,28.2c6.1,-0.6 11.7,-0.8 16.8,-0.8 23.5,0 37.3,5.1 55.5,15l-0.1,-0.1c86.8,47.7 182.4,93.2 288.7,110.9 6.2,1.4 11.4,2.9 15.9,4.4l9.2,-26.8c-5.7,-1.9 -11.9,-3.7 -19.1,-5.3l-0.4,-0.1 -0.4,-0.1C285.7,552.8 192.7,508.9 106.9,461.7l-0,-0 -0,-0C86.5,450.7 65.9,443.2 37.7,443.2l-0,0zM652.9,568.5 L647.6,573.8 549.5,671.9 579.7,674.6c135,12.1 340.2,1.1 540.2,14.6l3.7,-28 -470.7,-92.7zM662.3,599.2 L941.1,654.1C817.5,652.1 701.5,654.1 612.8,648.7l49.4,-49.4zM52.7,248c-29,0 -52.7,23.8 -52.7,52.7l0,532.4c0,29 23.8,52.7 52.7,52.7l66.5,0a14.2,14.2 0,0 0,10 -4.2L738.7,272.2a14.2,14.2 0,0 0,-10 -24.2l-676,0zM979.2,248a14.2,14.2 0,0 0,-10 4.2L359.7,861.7a14.2,14.2 0,0 0,10 24.2l712.2,0c29,0 52.7,-23.8 52.7,-52.7l0,-532.4c0,-29 -23.8,-52.7 -52.7,-52.7l-102.7,0zM52.7,276.4 L694.5,276.4 113.3,857.5 52.7,857.5c-13.8,0 -24.4,-10.6 -24.4,-24.4l0,-532.4c0,-13.8 10.6,-24.4 24.4,-24.4zM985.1,276.4 L1081.9,276.4c13.8,0 24.4,10.6 24.4,24.4l0,532.4c0,13.8 -10.6,24.4 -24.4,24.4l-677.9,0 581.1,-581.1z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307114"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="M915.1,130.3L985.8,201.1A38.6,38.6 58.5,0 1,985.8 255.6L238.6,1002.8A38.6,38.6 84,0 1,184.1 1002.8L113.3,932.1A38.6,38.6 92.7,0 1,113.3 877.6L860.6,130.3A38.6,38.6 79.6,0 1,915.1 130.3z"
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307515"/>
</vector>

View File

@ -1,11 +0,0 @@
<vector android:height="24dp" android:viewportHeight="637.7953"
android:viewportWidth="637.7953" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M638.4,462C518.5,688.5 338.4,648.4 266,598.8 159,525.6 124,422.3 242.9,288.3 455.8,48.3 302.5,13.2 302.5,13.2c0,0 182.3,30.4 27.3,256.1 -80.8,117.6 -110.4,189.8 -8,253.1 110,56.5 231,-61.9 257,-103z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m263.5,4.2c-41,12.5 -74,22 -103,59.8C60.4,194.3 29.6,305.7 128.9,407.1 251.6,489.6 425.3,370.6 425.3,370.6l38.5,66.6c0,0 -194.3,142.4 -338,52.1C79.6,460.2 -85.6,403.7 64.9,121.1 122.5,12.7 176.6,-10.9 263.5,4.2Z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="24dp" android:viewportHeight="1133.8583"
android:viewportWidth="1133.8583" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="M704.8,566.9A137.9,137.9 0,0 1,566.9 704.8,137.9 137.9,0 0,1 429,566.9 137.9,137.9 0,0 1,566.9 429,137.9 137.9,0 0,1 704.8,566.9ZM566.9,1098.1c-184.7,0 -26,-116.8 -185.9,-209.2 -160,-92.4 -181.8,103.5 -274.1,-56.4 -92.4,-160 88.2,-80.9 88.2,-265.6 0,-184.7 -180.5,-105.6 -88.2,-265.6 92.4,-160 114.1,35.9 274.1,-56.4 160,-92.4 1.2,-209.2 185.9,-209.2 184.7,-0 26,116.8 185.9,209.2 160,92.4 181.8,-103.5 274.1,56.4 92.4,160 -88.2,80.9 -88.2,265.6 0,184.7 180.5,105.6 88.2,265.6C934.6,992.5 912.8,796.6 752.8,888.9 592.9,981.3 751.6,1098.1 566.9,1098.1Z"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="68.95068359"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
<vector android:height="16dp" android:viewportHeight="566.92914"
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM177.2,124L354.3,124C432.9,124 496.1,182.6 496.1,265.7L496.1,336.6L549.2,336.6L460.6,425.2L372,336.6L425.2,336.6L425.2,265.7C425.2,226.4 393.6,194.9 354.3,194.9L248,194.9L177.2,124zM106.3,141.7L194.9,230.3L141.7,230.3L141.7,301.2C141.7,340.5 173.3,372 212.6,372L318.9,372L389.8,442.9L212.6,442.9C134,442.9 70.9,384.3 70.9,301.2L70.9,230.3L17.7,230.3L106.3,141.7z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
</vector>

View File

@ -1,7 +0,0 @@
<vector android:height="24dp" android:viewportHeight="42.519684"
android:viewportWidth="42.519684" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M15.18,9.94C15.08,9.91 15.02,9.91 15.01,9.94C14.88,10.06 14.74,10.21 14.61,10.35C14.32,10.68 14.04,11 13.75,11.32C13.39,11.73 13.03,12.13 12.67,12.53C12.28,12.95 11.89,13.37 11.49,13.79C11.11,14.2 10.72,14.62 10.33,15.04C9.96,15.44 9.58,15.83 9.21,16.24C8.88,16.61 8.55,16.97 8.21,17.33C7.9,17.65 7.6,18 7.29,18.34C7.2,18.44 7.11,18.53 7.02,18.63C6.83,18.76 6.46,19.18 6.08,19.65C5.96,19.77 5.85,19.89 5.73,20.01C5.5,20.25 5.27,20.49 5.04,20.73C5,20.77 4.97,20.81 4.93,20.85C4.92,20.88 4.98,20.95 5.08,21.03C4.98,21.2 4.92,21.33 4.96,21.36C5.04,21.42 5.11,21.49 5.18,21.56C5.41,21.78 5.64,21.99 5.88,22.2C6.2,22.47 6.53,22.73 6.86,22.99C7.25,23.3 7.64,23.59 8.03,23.9C8.44,24.21 8.84,24.54 9.24,24.86C9.65,25.2 10.05,25.54 10.45,25.88C10.86,26.23 11.27,26.58 11.67,26.94C12.08,27.3 12.48,27.66 12.89,28.02C13.28,28.38 13.68,28.74 14.07,29.1C14.46,29.47 14.86,29.83 15.25,30.19C15.64,30.55 16.03,30.91 16.41,31.27C16.47,31.33 16.53,31.38 16.59,31.44C16.77,31.61 19.22,29.07 19.04,28.89C18.98,28.83 18.91,28.77 18.84,28.71C18.45,28.34 18.06,27.97 17.66,27.6C17.27,27.24 16.87,26.88 16.48,26.52C16.08,26.15 15.68,25.78 15.28,25.42C14.87,25.05 14.45,24.68 14.04,24.32C13.62,23.94 13.2,23.58 12.77,23.21C12.36,22.86 11.94,22.5 11.51,22.16C11.1,21.82 10.68,21.49 10.26,21.15C10.09,21.02 9.91,20.88 9.74,20.75C9.96,20.5 10.18,20.25 10.41,20.01C10.7,19.68 11.01,19.36 11.31,19.04C11.65,18.67 11.98,18.3 12.32,17.93C12.69,17.54 13.05,17.14 13.42,16.75C13.8,16.34 14.19,15.92 14.58,15.51C14.98,15.08 15.38,14.65 15.78,14.21C16.14,13.8 16.5,13.39 16.87,12.98C17.14,12.67 17.4,12.36 17.69,12.08C17.84,11.92 17.99,11.76 18.13,11.6C18.23,11.41 15.85,10.11 15.18,9.94zM14.04,18.52L14.04,18.53C13.79,18.49 13.35,22 13.6,22.03L14.05,22.03L14.82,22.03L15.82,22.03C16.22,22.03 16.62,22.03 17.03,22.04C17.5,22.06 17.97,22.07 18.44,22.08C18.93,22.1 19.43,22.11 19.92,22.12C20.4,22.13 20.88,22.15 21.37,22.17C21.85,22.19 22.34,22.23 22.82,22.26C23.3,22.29 23.77,22.33 24.25,22.37C24.7,22.41 25.15,22.46 25.6,22.52C26.03,22.58 26.47,22.65 26.89,22.73C27.3,22.82 27.71,22.92 28.11,23.03C28.48,23.13 28.85,23.26 29.21,23.4C29.54,23.54 29.85,23.71 30.16,23.89C30.48,24.08 30.78,24.3 31.08,24.53C31.37,24.76 31.63,25.02 31.88,25.29C32.12,25.55 32.33,25.84 32.52,26.14C32.72,26.48 32.88,26.84 33.02,27.21C33.18,27.66 33.31,28.13 33.42,28.59C33.54,29.13 33.64,29.67 33.73,30.21C33.82,30.81 33.89,31.42 33.96,32.02C34.03,32.67 34.1,33.32 34.15,33.97C34.16,34.08 34.16,34.19 34.17,34.3C34.22,34.55 37.69,33.87 37.64,33.62C37.63,33.51 37.63,33.4 37.62,33.29C37.56,32.6 37.48,31.92 37.4,31.24C37.32,30.58 37.24,29.92 37.13,29.27C37.03,28.66 36.91,28.04 36.76,27.44C36.61,26.85 36.44,26.27 36.21,25.71C35.99,25.16 35.75,24.63 35.43,24.13C35.13,23.68 34.81,23.24 34.44,22.84C34.08,22.47 33.72,22.11 33.31,21.79C32.92,21.49 32.53,21.19 32.1,20.94C31.67,20.67 31.22,20.42 30.75,20.22C30.29,20.04 29.83,19.85 29.36,19.72C28.89,19.58 28.41,19.46 27.92,19.35C27.43,19.25 26.94,19.16 26.44,19.08C25.95,19.01 25.45,18.95 24.95,18.9C24.46,18.86 23.96,18.81 23.47,18.79C22.96,18.75 22.45,18.71 21.95,18.69C21.44,18.66 20.93,18.63 20.43,18.62C19.94,18.61 19.45,18.6 18.96,18.58C18.49,18.57 18.02,18.56 17.55,18.54C17.12,18.53 16.69,18.51 16.26,18.52L15.26,18.52L14.5,18.52L14.04,18.52z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>

View File

@ -3,12 +3,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#60000000">
<com.android.volley.toolbox.NetworkImageView
<uk.co.senab.photoview.PhotoView
android:id="@+id/view_media_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="fitCenter" />
android:layout_height="match_parent" />
</RelativeLayout>

View File

@ -3,16 +3,18 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:id="@+id/account_container">
<RelativeLayout
android:paddingTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@+id/account_avatar"
android:layout_marginRight="10dp" />
@ -20,6 +22,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/account_avatar">
@ -27,12 +30,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/account_display_name"
android:text="Display name"
android:textColor="?android:textColorPrimary"
android:textStyle="normal|bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\@username"
android:textColor="?android:textColorSecondary"
android:id="@+id/account_username" />
@ -45,6 +50,7 @@
android:layout_height="wrap_content"
android:id="@+id/account_note"
android:paddingTop="4dp"
android:paddingBottom="8dp"
android:textColor="?android:textColorTertiary" />
</LinearLayout>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title"
android:layout_centerInParent="true"
android:textAllCaps="true"
android:textStyle="normal|bold" />
</RelativeLayout>

View File

@ -10,6 +10,10 @@
android:title="@string/action_follow"
app:showAsAction="never" />
<item android:id="@+id/action_mute"
android:title="@string/action_mute"
app:showAsAction="never" />
<item android:id="@+id/action_block"
android:title="@string/action_block"
app:showAsAction="never" />

View File

@ -1,31 +0,0 @@
<?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_view_profile"
android:title="@string/action_view_profile"
app:showAsAction="never" />
<item
android:id="@+id/action_view_preferences"
android:title="@string/action_view_preferences"
app:showAsAction="never" />
<item
android:id="@+id/action_view_favourites"
android:title="@string/action_view_favourites"
app:showAsAction="never" />
<item
android:id="@+id/action_view_blocks"
android:title="@string/action_view_blocks"
app:showAsAction="never" />
<item
android:id="@+id/action_logout"
android:title="@string/action_logout"
app:showAsAction="never" />
</menu>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/status_follow"
android:title="@string/action_follow" />
<item
android:id="@+id/status_share"
android:title="@string/action_share"/>
<item android:title="@string/action_block"
android:id="@+id/status_block" />
<item android:title="@string/action_report"

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>

View File

@ -91,7 +91,7 @@
<string name="notification_follow_format">%s followed you</string>
<string name="report_username_format">Reporting @%s</string>
<string name="report_comment_hint">Additional Comments?</string>
<string name="report_comment_hint">Additional comments?</string>
<string name="action_compose">Compose</string>
<string name="action_login">Login with Mastodon</string>
@ -113,8 +113,8 @@
<string name="action_view_profile">Profile</string>
<string name="action_view_preferences">Preferences</string>
<string name="action_view_favourites">Favourites</string>
<string name="action_view_blocks">Blocked Users</string>
<string name="action_open_in_web">Open In Web</string>
<string name="action_view_blocks">Blocked users</string>
<string name="action_open_in_web">Open in browser</string>
<string name="action_set_time">Set</string>
<string name="confirmation_send">Toot!</string>
@ -143,18 +143,24 @@
<string name="notification_service_one_mention">Mention from %s</string>
<string name="pref_title_notification_settings">Notifications</string>
<string name="pref_title_pull_notifications">Enable Pull Notifcations</string>
<string name="pref_summary_pull_notifications">check for notifications periodically</string>
<string name="pref_title_pull_notification_check_interval">Check Interval</string>
<string name="pref_summary_pull_notification_check_interval">how often to pull</string>
<string name="pref_title_pull_notifications">Enable pull notifcations</string>
<string name="pref_summary_pull_notifications">Check for notifications periodically</string>
<string name="pref_title_pull_notification_check_interval">Check interval</string>
<string name="pref_summary_pull_notification_check_interval">How often to pull</string>
<string name="pref_title_notification_alert_sound">Notify with a sound</string>
<string name="pref_title_notification_style_vibrate">Notify with vibration</string>
<string name="pref_title_notification_style_light">Notify with light</string>
<string name="pref_title_appearance_settings">Appearance</string>
<string name="pref_title_light_theme">Use The Light Theme</string>
<string name="pref_title_light_theme">Use the Light Theme</string>
<string name="action_submit">Submit</string>
<string name="action_photo_pick">Add media</string>
<string name="action_compose_options">Privacy options</string>
<string name="login_success">Welcome back!</string>
<string name="action_share">Share</string>
<string name="send_status_to">Share toot URL to...</string>
<string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</string>
<string name="error_unmuting">That user wasn\'t unmuted.</string>
<string name="error_muting">That user wasn\'t muted.</string>
</resources>

View File

@ -55,6 +55,16 @@
<item name="notification_icon_tint">@color/notification_icon_tint_dark</item>
<item name="report_status_background_color">@color/report_status_background_dark</item>
<item name="report_status_divider_drawable">@drawable/report_status_divider_dark</item>
<item name="material_drawer_background">@color/color_primary_dark</item>
<item name="material_drawer_primary_text">@color/text_color_primary_dark</item>
<item name="material_drawer_primary_icon">@color/toolbar_icon_dark</item>
<item name="material_drawer_secondary_text">@color/text_color_secondary_dark</item>
<item name="material_drawer_hint_text">@color/text_color_tertiary_dark</item>
<item name="material_drawer_divider">@color/color_primary_dark_dark</item>
<item name="material_drawer_selected">@color/window_background_dark</item>
<item name="material_drawer_selected_text">@color/text_color_primary_dark</item>
<item name="material_drawer_header_selection_text">@color/text_color_primary_dark</item>
</style>
<style name="AppTheme.ImageButton.Dark" parent="@style/Widget.AppCompat.Button.Borderless.Colored">