Merge pull request #2 from Vavassor/master

Catching up with Vavassor/Tusky
main
Serage(pl) 8 years ago committed by GitHub
commit 180b103fc2
  1. 46
      CONTRIBUTING.md
  2. 2
      README.md
  3. 23
      app/build.gradle
  4. 12
      app/src/fdroid/AndroidManifest.xml
  5. 132
      app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java
  6. 18
      app/src/google/AndroidManifest.xml
  7. 121
      app/src/google/java/com/keylesspalace/tusky/MessagingService.java
  8. 2
      app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
  9. 38
      app/src/main/AndroidManifest.xml
  10. BIN
      app/src/main/ic_launcher-web.png
  11. 112
      app/src/main/ic_launcher.svg
  12. 2
      app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java
  13. 49
      app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
  14. 19
      app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java
  15. 40
      app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java
  16. 176
      app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java
  17. 26
      app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java
  18. 99
      app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
  19. 7
      app/src/main/java/com/keylesspalace/tusky/BaseFragment.java
  20. 26
      app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java
  21. 177
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  22. 12
      app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java
  23. 2
      app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java
  24. 2
      app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java
  25. 515
      app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java
  26. 129
      app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java
  27. 4
      app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java
  28. 11
      app/src/main/java/com/keylesspalace/tusky/IOUtils.java
  29. 82
      app/src/main/java/com/keylesspalace/tusky/LinkHelper.java
  30. 21
      app/src/main/java/com/keylesspalace/tusky/LinkListener.java
  31. 98
      app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
  32. 205
      app/src/main/java/com/keylesspalace/tusky/MainActivity.java
  33. 8
      app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java
  34. 105
      app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java
  35. 318
      app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java
  36. 224
      app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java
  37. 49
      app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java
  38. 98
      app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java
  39. 33
      app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java
  40. 110
      app/src/main/java/com/keylesspalace/tusky/SFragment.java
  41. 2
      app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java
  42. 4
      app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java
  43. 20
      app/src/main/java/com/keylesspalace/tusky/StatusRemoveListener.java
  44. 90
      app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java
  45. 31
      app/src/main/java/com/keylesspalace/tusky/StringWithEmoji.java
  46. 38
      app/src/main/java/com/keylesspalace/tusky/StringWithEmojiTypeAdapter.java
  47. 43
      app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java
  48. 47
      app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java
  49. 56
      app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java
  50. 38
      app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java
  51. 49
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
  52. 95
      app/src/main/java/com/keylesspalace/tusky/ViewMediaFragment.java
  53. 12
      app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java
  54. 14
      app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java
  55. 31
      app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java
  56. 12
      app/src/main/java/com/keylesspalace/tusky/entity/Account.java
  57. 19
      app/src/main/java/com/keylesspalace/tusky/entity/Profile.java
  58. 3
      app/src/main/java/com/keylesspalace/tusky/entity/Status.java
  59. 5
      app/src/main/res/color/drawer_visibility_panel_item.xml
  60. 12
      app/src/main/res/drawable/ic_camera_24dp.xml
  61. 9
      app/src/main/res/drawable/ic_check_24dp.xml
  62. 7
      app/src/main/res/drawable/ic_check_in_box_24dp.xml
  63. 9
      app/src/main/res/drawable/ic_email_24dp.xml
  64. 34
      app/src/main/res/drawable/ic_local_24dp.xml
  65. 9
      app/src/main/res/drawable/ic_lock_open_24dp.xml
  66. 9
      app/src/main/res/drawable/ic_lock_outline_24dp.xml
  67. 11
      app/src/main/res/drawable/ic_mute_24dp.xml
  68. 9
      app/src/main/res/drawable/ic_reject_24dp.xml
  69. 19
      app/src/main/res/drawable/ic_unmute_24dp.xml
  70. BIN
      app/src/main/res/drawable/tusky_logo.png
  71. 2
      app/src/main/res/layout/activity_account_list.xml
  72. 62
      app/src/main/res/layout/activity_compose.xml
  73. 137
      app/src/main/res/layout/activity_edit_profile.xml
  74. 35
      app/src/main/res/layout/activity_main.xml
  75. 0
      app/src/main/res/layout/fragment_account_list.xml
  76. 25
      app/src/main/res/layout/fragment_compose_options.xml
  77. 14
      app/src/main/res/layout/fragment_view_thread.xml
  78. 3
      app/src/main/res/layout/item_blocked_user.xml
  79. 80
      app/src/main/res/layout/item_follow_request.xml
  80. 25
      app/src/main/res/layout/item_footer_empty.xml
  81. 9
      app/src/main/res/layout/item_footer_end.xml
  82. 62
      app/src/main/res/layout/item_muted_user.xml
  83. 1
      app/src/main/res/layout/tab_account.xml
  84. 9
      app/src/main/res/menu/edit_profile_toolbar.xml
  85. 13
      app/src/main/res/menu/status_more.xml
  86. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  87. BIN
      app/src/main/res/mipmap-ldpi/ic_launcher.png
  88. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  89. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  90. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  91. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  92. 160
      app/src/main/res/values-ar/strings.xml
  93. 60
      app/src/main/res/values-de/strings.xml
  94. 117
      app/src/main/res/values-fr/strings.xml
  95. 164
      app/src/main/res/values-ja/strings.xml
  96. 154
      app/src/main/res/values-nl/strings.xml
  97. 159
      app/src/main/res/values-tr/strings.xml
  98. 35
      app/src/main/res/values/strings.xml
  99. 4
      app/src/main/res/xml/file_paths.xml

@ -0,0 +1,46 @@
# Contributing
## Getting Started
1. Fork the repository on the Github page by clicking the Fork button. This makes a fork of the project under your Github account.
2. Clone your fork to your machine. ```git clone https://github.com/<Your_Username>/Tusky```
3. Create a new branch named after your change. ```git checkout -b your-change-name``` (```checkout``` switches to a branch, ```-b``` specifies that the branch is a new one)
## Making Changes
### Text
All english text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```.
### Translation
Each translation has a single file that contains all of the text. A given locale's file can be found at ```app/src/main/res/values-<language code>[_<country code>]/strings.xml```. So, it could be ```values-en_US``` or ```values-es_ES```, for example. Specifically, they're the [two-letter ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the optional [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2), which is used for a dialect of that particular country.
If you're starting a translation that doesn't already exist, you can just copy the english ```strings.xml``` to a new ```values``` directory and replace the english text inside each of the ```<string>``` ```</string>``` pairs.
Strings follow XML rules, which means that apostrophes and quotation marks have to be "escaped" with a backslash like: ```shouldn\'t``` and ```\"formidable\"```. Also, formatting is ignored when shown in the application, so things like new lines have to be explicitly expressed with codes like ```\n``` for a new line. See also: [String Resources](https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling).
Please keep the organization and ordering of each of the strings the same as in the default ```strings.xml``` file. It just helps to keep so many translation files straight and up-to-date.
There are no icons or other resources needing localization, so it's just the text.
### Java
For java, I generally follow this [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. I encourage the use of optional annotations like ```@Nullable``` and ```@NotNull```. Also, if you ever make helper functions that take Android resources, annotations like ```@StringRes```, ```@DrawableRes```, and ```@AttrRes``` are helpful. They can prevent small errors, like accidentally passing an attribute id to a function that takes a drawable id, for example (both are ints).
### Visuals
There are two themes in the app, so any visual changes should be checked with both themes to ensure they look appropriate for both. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. For icons and drawables, use a white drawable and tint it at runtime using ```ThemeUtils``` and specify an attribute that references different colours depending on the theme. Do not reference attributes in drawable files, because it is only supported in API levels 21+.
### Saving
Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands:
```
git add .
git commit -m "Describe the changes in this commit here."
```
## Submitting Your Changes
1. Make sure your branch is up-to-date with the ```master``` branch. Run:
```
git fetch
git rebase origin/master
```
It may refuse to start the rebase if there's changes that haven't been committed, so make sure you've added and committed everything. If there were changes on master to any of the parts of files you worked on, a conflict will arise when you rebase. [Resolving a merge conflict](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line) is a good guide to help with this. After committing the resolution, you can run ```git rebase --continue``` to finish the rebase. If you want to cancel, like if you make some mistake in resolving the conflict, you can always do ```git rebase --abort```.
2. Push your local branch to your fork on Github by running ```git push origin your-change-name```.
3. Then, go to the original project page and make a pull request. Select your fork/branch and use ```master``` as the base branch.

@ -1,6 +1,6 @@
# Tusky
![](https://lh3.googleusercontent.com/6Ctl3PXaQi19qMaipWwzHAoKS9M9zy328cuulNZNAmRbjsPkSXs2xJ2OcyQNpOy23hI=w100)
![](app/src/main/res/drawable/tusky_logo.png)
Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is a GNU social-compatible federated social network. That means not one entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.

@ -12,6 +12,14 @@ android {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true
}
productFlavors {
google {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "true"
}
fdroid {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "false"
}
}
buildTypes {
release {
minifyEnabled true
@ -38,22 +46,23 @@ dependencies {
compile 'com.android.support:support-v13:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:exifinterface:25.3.1'
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.7.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
compile 'com.pkmmte.view:circularimageview:1.1'
compile 'com.github.peter9870:sparkbutton:master'
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.github.chrisbanes:PhotoView:1.3.1'
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.github.arimorty:floatingsearchview:2.0.3'
compile 'com.github.arimorty:floatingsearchview:2.0.4'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0'
compile 'com.jakewharton:butterknife:8.4.0'
compile 'com.google.firebase:firebase-messaging:10.0.1'
compile 'com.google.firebase:firebase-crash:10.0.1'
googleCompile 'com.google.firebase:firebase-messaging:10.0.1'
googleCompile 'com.google.firebase:firebase-crash:10.0.1'
testCompile 'junit:junit:4.12'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}
apply plugin: 'com.google.gms.google-services'

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<service
android:name=".MessagingService"
android:enabled="true"
android:exported="true" />
</application>
</manifest>

@ -0,0 +1,132 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.Spanned;
import android.util.ArraySet;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.Notification;
import java.util.HashSet;
import java.util.List;
import java.io.IOException;
import java.util.Set;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MessagingService extends IntentService {
public static final int NOTIFY_ID = 6; // This is an arbitrary number.
private MastodonAPI mastodonAPI;
public MessagingService() {
super("Tusky Pull Notification Service");
}
@Override
protected void onHandleIntent(Intent intent) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
boolean enabled = preferences.getBoolean("notificationsEnabled", true);
if (!enabled) {
return;
}
createMastodonApi();
mastodonAPI.notifications(null, null, null).enqueue(new Callback<List<Notification>>() {
@Override
public void onResponse(Call<List<Notification>> call,
Response<List<Notification>> response) {
if (response.isSuccessful()) {
onNotificationsReceived(response.body());
}
}
@Override
public void onFailure(Call<List<Notification>> call, Throwable t) {}
});
}
private void createMastodonApi() {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null);
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
@Override
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);
}
})
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
mastodonAPI = retrofit.create(MastodonAPI.class);
}
private void onNotificationsReceived(List<Notification> notificationList) {
SharedPreferences notificationsPreferences = getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
Set<String> currentIds = notificationsPreferences.getStringSet(
"current_ids", new HashSet<String>());
for (Notification notification : notificationList) {
String id = notification.id;
if (!currentIds.contains(id)) {
currentIds.add(id);
NotificationMaker.make(this, NOTIFY_ID, notification);
}
}
notificationsPreferences.edit()
.putStringSet("current_ids", currentIds)
.apply();
}
}

@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<service android:name=".MyFirebaseInstanceIdService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".MessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

@ -0,0 +1,121 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
* additional permission to convey the resulting work. */
package com.keylesspalace.tusky;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.Spanned;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.Notification;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MessagingService extends FirebaseMessagingService {
private MastodonAPI mastodonAPI;
private static final String TAG = "MessagingService";
public static final int NOTIFY_ID = 666;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, remoteMessage.getFrom());
Log.d(TAG, remoteMessage.toString());
String notificationId = remoteMessage.getData().get("notification_id");
if (notificationId == null) {
Log.e(TAG, "No notification ID in payload!!");
return;
}
Log.d(TAG, notificationId);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
boolean enabled = preferences.getBoolean("notificationsEnabled", true);
if (!enabled) {
return;
}
createMastodonAPI();
mastodonAPI.notification(notificationId).enqueue(new Callback<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
if (response.isSuccessful()) {
NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body());
}
}
@Override
public void onFailure(Call<Notification> call, Throwable t) {}
});
}
private void createMastodonAPI() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null);
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
@Override
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);
}
})
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
mastodonAPI = retrofit.create(MastodonAPI.class);
}
}

@ -33,7 +33,7 @@ import retrofit2.Response;
import retrofit2.Retrofit;
public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
private static final String TAG = "MyFirebaseInstanceIdService";
private static final String TAG = "com.keylesspalace.tusky.MyFirebaseInstanceIdService";
private TuskyAPI tuskyAPI;

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
@ -12,8 +13,9 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
android:theme="@style/AppTheme"
android:name=".TuskyApplication">
<activity android:name=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -59,24 +61,16 @@
<activity android:name=".ViewThreadActivity" />
<activity android:name=".ViewTagActivity" />
<activity android:name=".AccountActivity" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
<activity android:name=".BlocksActivity" />
<activity android:name=".AccountListActivity" />
<activity
android:name=".ReportActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
<service android:name=".MyFirebaseInstanceIdService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
<service android:name=".MyFirebaseMessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<activity
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" />
<receiver android:name=".NotificationClearBroadcastReceiver" />
@ -87,9 +81,19 @@
android:label="Compose Toot"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.keylesspalace.tusky.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 48 KiB

@ -0,0 +1,112 @@
<svg width="512" height="512" version="1.1" viewBox="0 0 135.46666 135.46666" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="c">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<clipPath id="g">
<circle cx="125" cy="125" r="112.52" fill="#2588d0"/>
</clipPath>
<clipPath id="e">
<g transform="matrix(.26458 0 0 .26458 7.7244 -29.561)" clip-path="url(#g)" fill="url(#a)" opacity=".05">
<g stroke-width=".5">
<path d="m98.535 73.473c-1.8371-0.09054-2.2298 0.10241-1.635 0.69722l70.711 70.711c-0.59481-0.59481-0.2021-0.78775 1.635-0.69722z" fill="url(#a)"/>
<path d="m96.9 74.17c0.70818 0.70818 2.8162 1.986 5.553 4.0332l70.711 70.711c-2.7368-2.0472-4.8448-3.3251-5.553-4.0332z" fill="url(#a)"/>
<path d="m102.45 78.203c2.4807 1.8557 4.9237 4.0232 7.1498 6.2492l70.711 70.711c-2.226-2.226-4.6691-4.3935-7.1498-6.2492z" fill="url(#a)"/>
<path d="m109.6 84.452c3.2825 3.2825 6.093 6.6922 7.8561 9.4168l70.711 70.711c-1.7631-2.7246-4.5736-6.1343-7.8561-9.4168z" fill="url(#a)"/>
<path d="m117.46 93.869c2.9912 4.6225 5.6692 8.3037 12.262 28.254l70.711 70.711c-6.5925-19.95-9.2705-23.631-12.262-28.254z" fill="url(#a)"/>
<path d="m129.72 122.12c2.4794 7.5032 5.7538 14.14 6.6211 16.113l70.711 70.711c-0.86733-1.9736-4.1417-8.6101-6.6211-16.113z" fill="url(#a)"/>
<path d="m136.34 138.24 1.3476 3.0059 70.711 70.711-1.3476-3.0059z" fill="url(#a)"/>
<path d="m137.69 141.24c-0.0653 0.0814-0.11927 0.14322-0.18554 0.22656l70.711 70.711c0.0663-0.0833 0.12024-0.14516 0.18554-0.22656z" fill="url(#a)"/>
<path d="m137.5 141.47-3.4395 3.0098 70.711 70.711 3.4395-3.0098z" fill="url(#a)"/>
<path d="m134.06 144.48c-5.5481 5.7637-16.033 11.691-24.285 13.729l70.711 70.711c8.2524-2.0372 18.737-7.9648 24.285-13.729z" fill="url(#a)"/>
<path d="m109.78 158.21c-6.3614 1.5706-21.301 1.1651-27.809 0.0137l70.711 70.711c6.5077 1.1514 21.447 1.5569 27.809-0.0137z" fill="url(#a)"/>
<path d="m81.971 158.22c-7.8527-1.3894-14.205-5.074-19.134-10.002l70.711 70.711c4.9282 4.9282 11.281 8.6128 19.134 10.002z" fill="url(#a)"/>
<path d="m62.837 148.22c-6.1069-6.1069-10.026-14.123-11.902-22.049l70.711 70.711c1.8752 7.9251 5.7946 15.942 11.902 22.049z" fill="url(#a)"/>
<path d="m50.936 126.17c-1.5987-9.5426 0.52116-16.959 1.6895-21.707l70.711 70.711c-1.1683 4.7477-3.2882 12.164-1.6894 21.707z" fill="url(#a)"/>
<path d="m52.625 104.46c2.6637-10.825 9.7356-20.465 18.479-26.402l70.711 70.711c-8.7429 5.9372-15.815 15.578-18.479 26.402z" fill="url(#a)"/>
<path d="m71.104 78.061c3.0009-2.0378 4.6792-2.7066 4.4597-2.9261l70.711 70.711c0.21947 0.21947-1.4588 0.88834-4.4597 2.9261z" fill="url(#a)"/>
<path d="m75.563 75.134c-0.11026-0.11026-0.69947-0.10709-1.8406-0.10709l70.711 70.711c1.1411 0 1.7303-3e-3 1.8406 0.10709z" fill="url(#a)"/>
<path d="m73.723 75.027c-3.9668 0-10.077 2.6389-14.275 6.0215l70.711 70.711c4.1987-3.3826 10.309-6.0215 14.275-6.0215z" fill="url(#a)"/>
<path d="m59.447 81.049c-8.0199 6.461-14.768 15.598-18.961 26.336l70.711 70.711c4.1931-10.738 10.941-19.875 18.961-26.336z" fill="url(#a)"/>
<path d="m40.486 107.38c-2.2433 6.6848-2.2747 5.5109-2.418 15.162l70.711 70.711c0.14324-9.6512 0.17463-8.4773 2.418-15.162z" fill="url(#a)"/>
<path d="m38.068 122.55c0.37633 6.9829 1.0389 9.3071 3.0723 15.406l70.711 70.711c-2.0333-6.0991-2.6959-8.4234-3.0723-15.406z" fill="url(#a)"/>
<path d="m41.141 137.95c2.3472 7.0404 6.2768 13.161 11.343 18.227l70.711 70.711c-5.0663-5.0663-8.9959-11.187-11.343-18.227z" fill="url(#a)"/>
<path d="m52.484 156.18c2.9744 2.9744 6.3406 5.5855 10.008 7.806l70.711 70.711c-3.6678-2.2205-7.034-4.8316-10.008-7.806z" fill="url(#a)"/>
<path d="m62.492 163.99c4.9985 3.0261 15.324 6.7023 22.098 7.8027l70.711 70.711c-6.7734-1.1005-17.099-4.7766-22.098-7.8027z" fill="url(#a)"/>
<path d="m84.59 171.79c1.8904 0.30967 4.992 0.90865 7.8848 1.1875l70.711 70.711c-2.8928-0.27885-5.9944-0.87783-7.8848-1.1875z" fill="url(#a)"/>
<path d="m92.475 172.98c12.195 0.94625 15.055-0.32666 24.506-1.8418l70.711 70.711c-9.4508 1.5151-12.311 2.788-24.506 1.8418z" fill="url(#a)"/>
<path d="m116.98 171.13c8.5906-2.4983 16.678-7.275 22.934-12.367l70.711 70.711c-6.2561 5.0922-14.343 9.8689-22.934 12.367z" fill="url(#a)"/>
<path d="m139.91 158.77 5.7871-4.7168 70.711 70.711-5.7871 4.7168z" fill="url(#a)"/>
<path d="m145.7 154.05c0.0891 0.11275 0.1968 0.26762 0.28321 0.375l70.711 70.711c-0.0864-0.10738-0.19411-0.26225-0.28321-0.375z" fill="url(#a)"/>
<path d="m145.98 154.43 1.9648 2.0644 70.711 70.711-1.9648-2.0644z" fill="url(#a)"/>
<path d="m147.95 156.49c0.29948 0.31455 0.59902 0.62179 0.89884 0.92161l70.711 70.711c-0.29982-0.29982-0.59936-0.60705-0.89884-0.9216z" fill="url(#a)"/>
<path d="m148.85 157.41c5.8411 5.8411 11.787 8.8672 19.377 8.303l70.711 70.711c-7.5898 0.56423-13.535-2.4619-19.377-8.303z" fill="url(#a)"/>
<path d="m168.22 165.71c7.5551-0.56163 13.041-2.6761 17.49-6.7422l70.711 70.711c-4.4488 4.066-9.9351 6.1806-17.49 6.7422z" fill="url(#a)"/>
<path d="m185.71 158.97c4.5132-4.1248 5.5354-6.236 7.6699-12.512l70.711 70.711c-2.1345 6.2758-3.1567 8.3869-7.6699 12.512z" fill="url(#a)"/>
<path d="m193.38 146.46c2.9552-8.6889 4.3184-16.193 3.9922-29.721l70.711 70.711c0.32618 13.527-1.0369 21.032-3.9922 29.721z" fill="url(#a)"/>
<path d="m197.38 116.74c-0.33294-13.818-1.6567-19.14-5.4434-23.453l70.711 70.711c3.7867 4.3128 5.1104 9.6355 5.4434 23.453z" fill="url(#a)"/>
<path d="m191.93 93.287c-0.0959-0.10288-0.19218-0.20269-0.28888-0.2994l70.711 70.711c0.0967 0.0967 0.19301 0.19652 0.28888 0.2994z" fill="url(#a)"/>
<path d="m191.64 92.988c-3.0781-3.0781-6.553-3.0044-8.5724 1.8287l70.711 70.711c2.0194-4.8331 5.4943-4.9068 8.5724-1.8287z" fill="url(#a)"/>
<path d="m183.07 94.816c-0.79737 1.9089-0.78178 2.91 0.11132 6.918l70.711 70.711c-0.8931-4.008-0.90869-5.0091-0.11132-6.918z" fill="url(#a)"/>
<path d="m183.18 101.73c1.6289 7.3102 1.7837 25.828 0.45313 32.072l70.711 70.711c1.3306-6.2445 1.1758-24.762-0.45313-32.072z" fill="url(#a)"/>
<path d="m183.64 133.81c-2.7904 13.095-11.463 17.859-22.207 12.49l70.711 70.711c10.744 5.3688 19.417 0.60451 22.207-12.49z" fill="url(#a)"/>
<path d="m161.43 146.3c-2.4274-1.213-3.8861-1.9048-5.748-3.5762l70.711 70.711c1.862 1.6714 3.3206 2.3632 5.748 3.5762z" fill="url(#a)"/>
<path d="m155.68 142.72c-0.18578-0.19582-0.32768-0.35348-0.50391-0.54101l70.711 70.711c0.17623 0.18753 0.31813 0.34519 0.50391 0.54101z" fill="url(#a)"/>
<path d="m155.18 142.18c1.047-1.7899 1.5781-3.0116 2.7754-4.9883l70.711 70.711c-1.1973 1.9767-1.7284 3.1984-2.7754 4.9883z" fill="url(#a)"/>
<path d="m157.95 137.19c4.9715-8.2077 9.7196-21.436 11.604-32.795l70.711 70.711c-1.8839 11.359-6.632 24.587-11.604 32.795z" fill="url(#a)"/>
<path d="m169.56 104.4c1.1273-6.797 0.81713-9.1729-0.83126-10.821l70.711 70.711c1.6484 1.6484 1.9586 4.0243 0.83126 10.821z" fill="url(#a)"/>
<path d="m168.73 93.575c-0.32731-0.32731-0.70738-0.62594-1.1394-0.92479l70.711 70.711c0.43206 0.29886 0.81213 0.59748 1.1394 0.92479z" fill="url(#a)"/>
<path d="m167.59 92.65c-2.0094-0.98322-2.8331-1.0491-4.9531-0.39844l70.711 70.711c2.12-0.65064 2.9438-0.58478 4.9531 0.39844z" fill="url(#a)"/>
<path d="m162.63 92.252c-1.3988 0.42933-3.0221 1.3082-3.6055 1.9531l70.711 70.711c0.58337-0.64492 2.2067-1.5238 3.6055-1.9531z" fill="url(#a)"/>
<path d="m159.03 94.205c-0.58363 0.64492-1.9734 4.6467-3.0879 8.8945l70.711 70.711c1.1145-4.2479 2.5043-8.2496 3.0879-8.8945z" fill="url(#a)"/>
<path d="m155.94 103.1c-1.1144 4.2479-2.7555 9.2858-3.6465 11.193l70.711 70.711c0.89102-1.9076 2.532-6.9455 3.6465-11.193z" fill="url(#a)"/>
<path d="m152.29 114.29c-0.89099 1.9076-1.8493 4.2376-2.1309 5.1777l70.711 70.711c0.28157-0.94013 1.2399-3.2702 2.1309-5.1777z" fill="url(#a)"/>
<path d="m150.16 119.47c-0.28161 0.94013-1.2853 3.2472-2.2305 5.127l70.711 70.711c0.94515-1.8798 1.9489-4.1868 2.2305-5.127z" fill="url(#a)"/>
<path d="m147.93 124.6-2 4.0059 70.711 70.711 2-4.0059z" fill="url(#a)"/>
<path d="m145.93 128.6-1.1016-2.5469 70.711 70.711 1.1016 2.5469z" fill="url(#a)"/>
<path d="m144.83 126.06c-0.50786-1.1277-2.6738-6.8816-5.1504-12.898l70.711 70.711c2.4766 6.0168 4.6425 11.771 5.1504 12.898z" fill="url(#a)"/>
<path d="m139.68 113.16c-5.7036-13.857-11.141-23.408-17.401-29.668l70.711 70.711c6.2598 6.2598 11.697 15.811 17.401 29.668z" fill="url(#a)"/>
<path d="m122.28 83.49c-6.0835-6.0835-12.944-9.058-21.58-9.8534l70.711 70.711c8.6358 0.79538 15.496 3.7699 21.58 9.8534z" fill="url(#a)"/>
<path d="m100.7 73.637c-0.8718-0.08029-1.5892-0.13573-2.1641-0.16406l70.711 70.711c0.57491 0.0283 1.2923 0.0838 2.1641 0.16407z" fill="url(#a)"/>
</g>
<path d="m98.535 73.473c-4.0243-0.19833-1.1175 0.96369 3.918 4.7305 6.1387 4.592 12.047 11.094 15.006 15.666 2.9912 4.6225 5.6692 8.3037 12.262 28.254 2.4794 7.5032 5.7538 14.14 6.6211 16.113l1.3476 3.0059c-0.0653 0.0814-0.11927 0.14322-0.18554 0.22656l-3.4395 3.0098c-5.5481 5.7637-16.033 11.691-24.285 13.729-6.3614 1.5706-21.301 1.1651-27.809 0.0137-17.584-3.1111-27.647-17.73-31.035-32.051-1.5987-9.5426 0.52116-16.959 1.6895-21.707 2.6637-10.825 9.7356-20.465 18.479-26.402 4.5085-3.0615 6.0316-3.0332 2.6191-3.0332-3.9668 0-10.077 2.6389-14.275 6.0215-8.0199 6.461-14.768 15.598-18.961 26.336-2.2433 6.6848-2.2747 5.5109-2.418 15.162 0.37633 6.9829 1.0389 9.3071 3.0723 15.406 3.7252 11.174 11.436 20.03 21.352 26.033 4.9985 3.0261 15.324 6.7023 22.098 7.8027 1.8904 0.30967 4.992 0.90865 7.8848 1.1875 12.195 0.94625 15.055-0.32666 24.506-1.8418 8.5906-2.4983 16.678-7.275 22.934-12.367l5.7871-4.7168c0.0891 0.11275 0.1968 0.26762 0.28321 0.375l1.9648 2.0644c6.134 6.4426 12.296 9.8178 20.275 9.2246 7.5551-0.56163 13.041-2.6761 17.49-6.7422 4.5132-4.1248 5.5354-6.236 7.6699-12.512 2.9552-8.6889 4.3184-16.193 3.9922-29.721-0.33294-13.818-1.6567-19.14-5.4434-23.453-3.1472-3.3774-6.7785-3.4556-8.8613 1.5293-0.79737 1.9089-0.78178 2.91 0.11132 6.918 1.6289 7.3102 1.7837 25.828 0.45313 32.072-2.7904 13.095-11.463 17.859-22.207 12.49-2.4274-1.213-3.8861-1.9048-5.748-3.5762-0.18578-0.19582-0.32768-0.35348-0.50391-0.54101 1.047-1.7899 1.5781-3.0116 2.7754-4.9883 4.9715-8.2077 9.7196-21.436 11.604-32.795 1.3511-8.1466 0.63728-9.9421-1.9707-11.746-2.0094-0.98322-2.8331-1.0491-4.9531-0.39844-1.3988 0.42933-3.0221 1.3082-3.6055 1.9531-0.58363 0.64492-1.9734 4.6467-3.0879 8.8945-1.1144 4.2479-2.7555 9.2858-3.6465 11.193-0.89099 1.9076-1.8493 4.2376-2.1309 5.1777-0.28161 0.94013-1.2853 3.2472-2.2305 5.127l-2 4.0059-1.1016-2.5469c-0.50786-1.1277-2.6738-6.8816-5.1504-12.898-11.247-27.323-21.459-37.908-38.98-39.522-0.8718-0.080295-1.5892-0.13573-2.1641-0.16406z" fill="url(#a)"/>
</g>
</clipPath>
<linearGradient id="a" x1="146.2" x2="201.21" y1="153.65" y2="210.2" gradientUnits="userSpaceOnUse" xlink:href="#c"/>
<clipPath id="f">
<circle transform="matrix(1.0037 0 0 1.0037 1.3836 -1.2671)" cx="40.292" cy="2.6396" r="29.772" clip-path="url(#e)" opacity=".1"/>
</clipPath>
<linearGradient id="b" x1="45.701" x2="61.309" y1="6.5707" y2="22.179" gradientUnits="userSpaceOnUse">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<filter id="d" x="-.024" y="-.024" width="1.048" height="1.048" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="13.532652"/>
</filter>
</defs>
<g transform="translate(-8.0628 99.804)">
<g transform="matrix(2 0 0 2 -5.3112 -36.986)">
<g transform="matrix(.046875 0 0 .046875 6.3858 34.613)">
<g>
<circle transform="matrix(.98504 0 0 .98504 29.403 -5.6005)" cx="710.14" cy="-672.09" r="676.63" filter="url(#d)" opacity=".36"/>
<ellipse cx="728.91" cy="-684.4" rx="663.22" ry="661.64" fill="#393f4f"/>
<ellipse cx="725.02" cy="-684.4" rx="597.02" ry="595.6" fill="#2588d0"/>
<circle transform="matrix(20.463 0 0 20.414 -95.572 -724.78)" cx="40.292" cy="1.9781" r="29.176" clip-path="url(#f)" fill="url(#b)" opacity=".1"/>
</g>
<g transform="matrix(21.768 0 0 21.716 -148.18 -741.68)" stroke-width=".052917">
<g stroke="#efefef">
<g stroke="#efefef" stroke-width=".052917">
<g transform="matrix(1.0012 0 0 1.0012 -.2806 -.13189)" stroke-width=".052853">
<path d="m33.997 14.956c-6.7505-0.65592-11.276-4.003-12.835-9.4932-0.97232-3.4236-0.40007-6.6554 1.7689-9.9897 1.963-3.0177 4.5842-4.9793 6.6538-4.9793h0.55721l-0.68124 0.45556c-2.3327 1.5599-4.1732 3.9523-5.0456 6.5585-0.51198 1.5295-0.64528 4.5724-0.27628 6.3067 0.63604 2.9894 2.4635 5.3876 5.0854 6.6734 1.7788 0.87238 3.409 1.2191 5.7324 1.2191 3.8422 0 6.9587-1.203 9.6137-3.711l0.90726-0.85704-0.45582-1.0092c-0.40414-0.89475-1.3238-3.3221-3.0074-7.9377-0.66266-1.8167-1.3902-3.0367-2.6217-4.3966-0.83567-0.92274-2.4857-2.3558-3.8598-3.3523-0.41933-0.3041-0.41152-0.30624 0.78481-0.21562 4.547 0.34444 6.8542 2.8969 10.192 11.276 0.58286 1.463 0.84878 2.0983 0.96426 2.3599 0.02953 0.0669 0.07669 0.21387 0.13247 0.16887 0.05579-0.044998 0.2425-0.39246 0.30776-0.50578 0.06527-0.11332 0.13756-0.25964 0.22146-0.43667 0.0839-0.17703 0.1794-0.38476 0.29108-0.62088 0.81832-1.7301 1.8142-4.5739 2.1755-6.2124 0.17685-0.80191 0.31139-1.0422 0.6944-1.2403 1.2546-0.64879 2.3739-0.046158 2.3739 1.2782 0 2.3978-1.562 7.2043-3.3553 10.325-0.11458 0.1994-0.2299 0.32992-0.28958 0.44825-0.05969 0.11833-0.22117 0.32942-0.18689 0.38982 0.0262 0.046164 0.03631 0.058028 0.07857 0.1003 0.04494 0.044952 0.10546 0.1025 0.18606 0.16729 0.59444 0.47791 2.1261 1.0701 2.9132 1.178 1.0012 0.13722 2.1429-0.34975 2.7808-1.186 1.4436-1.8927 1.7772-5.2915 1.0067-10.256-0.28139-1.8131-0.2804-1.8507 0.0612-2.3095 0.8275-1.1115 1.8285-0.9019 2.5288 0.52951 1.4339 2.9307 1.2081 10.094-0.43371 13.76-0.9119 2.0362-2.4949 3.1576-5.135 3.6376-2.1119 0.38401-3.6271-0.10683-5.3738-1.7407l-1.0573-0.98905-0.49724 0.52519c-1.2916 1.3642-4.2775 3.0339-6.4849 3.6264-1.0871 0.29178-3.7581 0.64007-4.619 0.60228-0.28227-0.01239-1.0906-0.07862-1.7963-0.14719z" fill="#fefefe" stroke="#e9e9eb"/>
<path d="m49.244 6.7482c-0.30781-0.38303-0.80659-1.1368-1.1084-1.6749l-0.54874-0.97853 0.47196-0.85537c0.25958-0.47046 0.64637-1.3655 0.89107-1.9821l0.39948-0.98883 1.3378 1.3563c0.8003 0.81132 1.266 1.4388 1.2301 1.6104-0.08034 0.3845-1.217 2.7582-1.713 3.561l-0.40066 0.6485z" fill="#e9e9eb" stroke="#efefef"/>
</g>
</g>
</g>
<path d="m45.251 7.0162s0.6208 1.4785 1.9195 3.2142" fill="none" stroke="#e9e9eb"/>
</g>
</g>
</g>
</g>
<style>.st0{fill:#e0e0e0}.st1{fill:#fff}.st2{clip-path:url(#SVGID_2_);fill:#fbbc05}.st3{clip-path:url(#SVGID_4_);fill:#ea4335}.st4{clip-path:url(#SVGID_6_);fill:#34a853}.st5{clip-path:url(#SVGID_8_);fill:#4285f4}</style>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@ -17,5 +17,7 @@ package com.keylesspalace.tusky;
interface AccountActionListener {
void onViewAccount(String id);
void onMute(final boolean mute, final String id, final int position);
void onBlock(final boolean block, final String id, final int position);
void onRespondToFollowRequest(final boolean accept, final String id, final int position);
}

@ -28,11 +28,11 @@ import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -54,7 +54,7 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AccountActivity extends BaseActivity {
public class AccountActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
private static final String TAG = "AccountActivity"; // logging tag
private String accountId;
@ -63,6 +63,7 @@ public class AccountActivity extends BaseActivity {
private boolean muting = false;
private boolean isSelf;
private TabLayout tabLayout;
private AccountPagerAdapter pagerAdapter;
private Account loadedAccount;
@BindView(R.id.account_locked) ImageView accountLockedView;
@ -80,8 +81,7 @@ public class AccountActivity extends BaseActivity {
accountId = intent.getStringExtra("id");
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
String loggedInAccountId = preferences.getString("loggedInAccountId", null);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
@ -142,6 +142,7 @@ public class AccountActivity extends BaseActivity {
// Setup the tabs and timeline pager.
AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), this,
accountId);
pagerAdapter = adapter;
String[] pageTitles = {
getString(R.string.title_statuses),
getString(R.string.title_follows),
@ -165,6 +166,12 @@ public class AccountActivity extends BaseActivity {
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("accountId", accountId);
super.onSaveInstanceState(outState);
}
private void obtainAccount() {
mastodonAPI.account(accountId).enqueue(new Callback<Account>() {
@Override
@ -183,12 +190,6 @@ public class AccountActivity extends BaseActivity {
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("accountId", accountId);
super.onSaveInstanceState(outState);
}
private void onObtainAccountSuccess(Account account) {
loadedAccount = account;
@ -204,9 +205,21 @@ public class AccountActivity extends BaseActivity {
displayName.setText(account.getDisplayName());
note.setText(account.note);
note.setLinksClickable(true);
note.setMovementMethod(LinkMovementMethod.getInstance());
LinkHelper.setClickableText(note, account.note, null, new LinkListener() {
@Override
public void onViewTag(String tag) {
Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class);
intent.putExtra("hashtag", tag);
startActivity(intent);
}
@Override
public void onViewAccount(String id) {
Intent intent = new Intent(AccountActivity.this, AccountActivity.class);
intent.putExtra("id", id);
startActivity(intent);
}
});
if (account.locked) {
accountLockedView.setVisibility(View.VISIBLE);
@ -289,6 +302,16 @@ public class AccountActivity extends BaseActivity {
updateButtons();
}
@Override
public void onUserRemoved(String accountId) {
for (Fragment fragment : pagerAdapter.getRegisteredFragments()) {
if (fragment instanceof StatusRemoveListener) {
StatusRemoveListener listener = (StatusRemoveListener) fragment;
listener.removePostsByUser(accountId);
}
}
}
private void updateFollowButton(FloatingActionButton button) {
if (following) {
button.setImageResource(R.drawable.ic_person_minus_24px);

@ -15,6 +15,7 @@
package com.keylesspalace.tusky;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import com.keylesspalace.tusky.entity.Account;
@ -64,6 +65,24 @@ abstract class AccountAdapter extends RecyclerView.Adapter {
notifyItemRangeInserted(end, newAccounts.size());
}
@Nullable
Account removeItem(int position) {
if (position < 0 || position >= accountList.size()) {
return null;
}
Account account = accountList.remove(position);
notifyItemRemoved(position);
return account;
}
void addItem(Account account, int position) {
if (position < 0 || position > accountList.size()) {
return;
}
accountList.add(position, account);
notifyItemInserted(position);
}
public Account getItem(int position) {
if (position >= 0 && position < accountList.size()) {
return accountList.get(position);

@ -15,6 +15,7 @@
package com.keylesspalace.tusky;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
@ -23,23 +24,54 @@ import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
public class BlocksActivity extends BaseActivity {
public class AccountListActivity extends BaseActivity {
enum Type {
BLOCKS,
MUTES,
FOLLOW_REQUESTS,
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blocks);
setContentView(R.layout.activity_account_list);
Type type;
Intent intent = getIntent();
if (intent != null) {
type = (Type) intent.getSerializableExtra("type");
} else {
type = Type.BLOCKS;
}
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setTitle(getString(R.string.title_blocks));
switch (type) {
case BLOCKS: { bar.setTitle(getString(R.string.title_blocks)); break; }
case MUTES: { bar.setTitle(getString(R.string.title_mutes)); break; }
case FOLLOW_REQUESTS: {
bar.setTitle(getString(R.string.title_follow_requests));
break;
}
}
bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true);
}
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = AccountFragment.newInstance(AccountFragment.Type.BLOCKS);
AccountListFragment.Type fragmentType;
switch (type) {
default:
case BLOCKS: { fragmentType = AccountListFragment.Type.BLOCKS; break; }
case MUTES: { fragmentType = AccountListFragment.Type.MUTES; break; }
case FOLLOW_REQUESTS: {
fragmentType = AccountListFragment.Type.FOLLOW_REQUESTS;
break;
}
}
Fragment fragment = AccountListFragment.newInstance(fragmentType);
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}

@ -20,6 +20,7 @@ import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
@ -37,16 +38,15 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AccountFragment extends BaseFragment implements AccountActionListener {
private static final String TAG = "Account"; // logging tag
private Call<List<Account>> listCall;
public class AccountListFragment extends BaseFragment implements AccountActionListener {
private static final String TAG = "AccountList"; // logging tag
public enum Type {
FOLLOWS,
FOLLOWERS,
BLOCKS,
MUTES,
FOLLOW_REQUESTS,
}
private Type type;
@ -58,18 +58,18 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
private TabLayout.OnTabSelectedListener onTabSelectedListener;
private MastodonAPI api;
public static AccountFragment newInstance(Type type) {
public static AccountListFragment newInstance(Type type) {
Bundle arguments = new Bundle();
AccountFragment fragment = new AccountFragment();
arguments.putString("type", type.name());
AccountListFragment fragment = new AccountListFragment();
arguments.putSerializable("type", type);
fragment.setArguments(arguments);
return fragment;
}
public static AccountFragment newInstance(Type type, String accountId) {
public static AccountListFragment newInstance(Type type, String accountId) {
Bundle arguments = new Bundle();
AccountFragment fragment = new AccountFragment();
arguments.putString("type", type.name());
AccountListFragment fragment = new AccountListFragment();
arguments.putSerializable("type", type);
arguments.putString("accountId", accountId);
fragment.setArguments(arguments);
return fragment;
@ -79,7 +79,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
type = Type.valueOf(arguments.getString("type"));
type = (Type) arguments.getSerializable("type");
accountId = arguments.getString("accountId");
api = null;
}
@ -89,7 +89,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_account, container, false);
View rootView = inflater.inflate(R.layout.fragment_account_list, container, false);
Context context = getContext();
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
@ -105,6 +105,10 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
scrollListener = null;
if (type == Type.BLOCKS) {
adapter = new BlocksAdapter(this);
} else if (type == Type.MUTES) {
adapter = new MutesAdapter(this);
} else if (type == Type.FOLLOW_REQUESTS) {
adapter = new FollowRequestsAdapter(this);
} else {
adapter = new FollowAdapter(this);
}
@ -154,12 +158,6 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
recyclerView.addOnScrollListener(scrollListener);
}
@Override
public void onDestroy() {
super.onDestroy();
if (listCall != null) listCall.cancel();
}
@Override
public void onDestroyView() {
if (jumpToTopAllowed()) {
@ -186,6 +184,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
}
};
Call<List<Account>> listCall;
switch (type) {
default:
case FOLLOWS: {
@ -204,6 +203,10 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
listCall = api.mutes(fromId, uptoId, null);
break;
}
case FOLLOW_REQUESTS: {
listCall = api.followRequests(fromId, uptoId, null);
break;
}
}
callList.add(listCall);
listCall.enqueue(cb);
@ -236,12 +239,78 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
Log.e(TAG, "Fetch failure: " + exception.getMessage());
}
@Override
public void onViewAccount(String id) {
Intent intent = new Intent(getContext(), AccountActivity.class);
intent.putExtra("id", id);
startActivity(intent);
}
@Override
public void onMute(final boolean mute, final String id, final int position) {
if (api == null) {
/* If somehow an unmute button is clicked after onCreateView but before
* onActivityCreated, then this would get called with a null api object, so this eats
* that input. */
Log.d(TAG, "MastodonAPI isn't initialised so this mute can't occur.");
return;
}
Callback<Relationship> callback = new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
if (response.isSuccessful()) {
onMuteSuccess(mute, id, position);
} else {
onMuteFailure(mute, id);
}
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onMuteFailure(mute, id);
}
};
Call<Relationship> call;
if (!mute) {
call = api.unmuteAccount(id);
} else {
call = api.muteAccount(id);
}
callList.add(call);
call.enqueue(callback);
}
private void onMuteSuccess(boolean muted, final String id, final int position) {
if (muted) {
return;
}
final MutesAdapter mutesAdapter = (MutesAdapter) adapter;
final Account unmutedUser = mutesAdapter.removeItem(position);
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mutesAdapter.addItem(unmutedUser, position);
onMute(true, id, position);
}
};
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo, listener)
.show();
}
private void onMuteFailure(boolean mute, String id) {
String verb;
if (mute) {
verb = "mute";
} else {
verb = "unmute";
}
Log.e(TAG, String.format("Failed to %s account id %s", verb, id));
}
@Override
public void onBlock(final boolean block, final String id, final int position) {
if (api == null) {
/* If somehow an unblock button is clicked after onCreateView but before
@ -255,7 +324,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
if (response.isSuccessful()) {
onBlockSuccess(block, position);
onBlockSuccess(block, id, position);
} else {
onBlockFailure(block, id);
}
@ -277,9 +346,22 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
call.enqueue(cb);
}
private void onBlockSuccess(boolean blocked, int position) {
BlocksAdapter blocksAdapter = (BlocksAdapter) adapter;
blocksAdapter.setBlocked(blocked, position);
private void onBlockSuccess(boolean blocked, final String id, final int position) {
if (blocked) {
return;
}
final BlocksAdapter blocksAdapter = (BlocksAdapter) adapter;
final Account unblockedUser = blocksAdapter.removeItem(position);
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
blocksAdapter.addItem(unblockedUser, position);
onBlock(true, id, position);
}
};
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo, listener)
.show();
}
private void onBlockFailure(boolean block, String id) {
@ -292,8 +374,56 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
Log.e(TAG, String.format("Failed to %s account id %s", verb, id));
}
@Override
public void onRespondToFollowRequest(final boolean accept, final String accountId,
final int position) {
if (api == null) {
/* If somehow an response button is clicked after onCreateView but before
* onActivityCreated, then this would get called with a null api object, so this eats
* that input. */
Log.d(TAG, "MastodonAPI isn't initialised, so follow requests can't be responded to.");
return;
}
Callback<Relationship> callback = new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
if (response.isSuccessful()) {
onRespondToFollowRequestSuccess(position);
} else {
onRespondToFollowRequestFailure(accept, accountId);
}
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onRespondToFollowRequestFailure(accept, accountId);
}
};
Call<Relationship> call;
if (accept) {
call = api.authorizeFollowRequest(accountId);
} else {
call = api.rejectFollowRequest(accountId);
}
callList.add(call);
call.enqueue(callback);
}
private void onRespondToFollowRequestSuccess(int position) {
FollowRequestsAdapter followRequestsAdapter = (FollowRequestsAdapter) adapter;
followRequestsAdapter.removeItem(position);
}
private void onRespondToFollowRequestFailure(boolean accept, String accountId) {
String verb = (accept) ? "accept" : "reject";
String message = String.format("Failed to %s account id %s.", verb, accountId);
Log.e(TAG, message);
}
private boolean jumpToTopAllowed() {
return type != Type.BLOCKS;
return type == Type.FOLLOWS || type == Type.FOLLOWERS;
}
private void jumpToTop() {

@ -24,15 +24,20 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
class AccountPagerAdapter extends FragmentPagerAdapter {
private Context context;
private String accountId;
private String[] pageTitles;
private List<Fragment> registeredFragments;
AccountPagerAdapter(FragmentManager manager, Context context, String accountId) {
super(manager);
this.context = context;
this.accountId = accountId;
registeredFragments = new ArrayList<>();
}
void setPageTitles(String[] titles) {
@ -46,10 +51,10 @@ class AccountPagerAdapter extends FragmentPagerAdapter {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId);
}
case 1: {
return AccountFragment.newInstance(AccountFragment.Type.FOLLOWS, accountId);
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, accountId);
}
case 2: {
return AccountFragment.newInstance(AccountFragment.Type.FOLLOWERS, accountId);
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, accountId);
}
default: {
return null;
@ -73,4 +78,21 @@ class AccountPagerAdapter extends FragmentPagerAdapter {
title.setText(pageTitles[position]);
return view;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.add(fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove((Fragment) object);
super.destroyItem(container, position, object);
}
List<Fragment> getRegisteredFragments() {
return registeredFragments;
}
}

@ -15,6 +15,8 @@
package com.keylesspalace.tusky;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -22,6 +24,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
@ -29,7 +32,6 @@ import android.text.Spanned;
import android.util.TypedValue;
import android.view.Menu;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -46,16 +48,13 @@ import retrofit2.Callback;
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 {
private static final String TAG = "BaseActivity"; // logging tag
protected MastodonAPI mastodonAPI;
protected TuskyAPI tuskyAPI;
protected Dispatcher mastodonApiDispatcher;
protected PendingIntent serviceAlarmIntent;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -65,6 +64,9 @@ public class BaseActivity extends AppCompatActivity {
createMastodonAPI();
createTuskyAPI();
/* 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. */
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
@ -96,8 +98,12 @@ public class BaseActivity extends AppCompatActivity {
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
}
protected SharedPreferences getPrivatePreferences() {
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
}
protected String getAccessToken() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
return preferences.getString("accessToken", null);
}
@ -107,7 +113,7 @@ public class BaseActivity extends AppCompatActivity {
}
protected String getBaseUrl() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
return "https://" + preferences.getString("domain", null);
}
@ -116,6 +122,7 @@ public class BaseActivity extends AppCompatActivity {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
.create();
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
@ -148,17 +155,18 @@ public class BaseActivity extends AppCompatActivity {
}
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build();
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);
tuskyAPI = retrofit.create(TuskyAPI.class);
}
}
protected void redirectIfNotLoggedIn() {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
String domain = preferences.getString("domain", null);
String accessToken = preferences.getString("accessToken", null);
if (domain == null || accessToken == null) {
@ -188,30 +196,49 @@ public class BaseActivity extends AppCompatActivity {
}
protected void enablePushNotifications() {
tuskyAPI.register(getBaseUrl(), getAccessToken(), FirebaseInstanceId.getInstance().getToken()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Enable push notifications response: " + response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Enable push notifications failed: " + t.getMessage());
}
});
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
String token = com.google.firebase.iid.FirebaseInstanceId.getInstance().getToken();
tuskyAPI.register(getBaseUrl(), getAccessToken(), token).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Enable push notifications response: " + response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Enable push notifications failed: " + t.getMessage());
}
});
} else {
// Start up the MessagingService on a repeating interval for "pull" notifications.
long checkInterval = 60 * 1000 * 5;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, MessagingService.class);
final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
}
}
protected void disablePushNotifications() {
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Disable push notifications response: " + response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Disable push notifications failed: " + t.getMessage());
}
});
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
Log.d(TAG, "Disable push notifications response: " + response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Disable push notifications failed: " + t.getMessage());
}
});
} else if (serviceAlarmIntent != null) {
// Cancel the repeating call for "pull" notifications.
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(serviceAlarmIntent);
}
}
}

@ -15,6 +15,8 @@
package com.keylesspalace.tusky;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
@ -40,4 +42,9 @@ public class BaseFragment extends Fragment {
}
super.onDestroy();
}
protected SharedPreferences getPrivatePreferences() {
return getContext().getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
}
}

@ -26,9 +26,6 @@ import com.keylesspalace.tusky.entity.Account;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
import java.util.HashSet;
import java.util.Set;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -36,11 +33,8 @@ class BlocksAdapter extends AccountAdapter {
private static final int VIEW_TYPE_BLOCKED_USER = 0;
private static final int VIEW_TYPE_FOOTER = 1;
private Set<Integer> unblockedAccountPositions;
BlocksAdapter(AccountActionListener accountActionListener) {
super(accountActionListener);
unblockedAccountPositions = new HashSet<>();
}
@Override
@ -65,8 +59,7 @@ class BlocksAdapter extends AccountAdapter {
if (position < accountList.size()) {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
boolean blocked = !unblockedAccountPositions.contains(position);
holder.setupActionListener(accountActionListener, blocked, position);
holder.setupActionListener(accountActionListener, true);
}
}
@ -79,15 +72,6 @@ class BlocksAdapter extends AccountAdapter {
}
}
void setBlocked(boolean blocked, int position) {
if (blocked) {
unblockedAccountPositions.remove(position);
} else {
unblockedAccountPositions.add(position);
}
notifyItemChanged(position);
}
static class BlockedUserViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.blocked_user_avatar) CircularImageView avatar;
@BindView(R.id.blocked_user_username) TextView username;
@ -114,12 +98,14 @@ class BlocksAdapter extends AccountAdapter {
.into(avatar);
}
void setupActionListener(final AccountActionListener listener, final boolean blocked,
final int position) {
void setupActionListener(final AccountActionListener listener, final boolean blocked) {
unblock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onBlock(!blocked, id, position);
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onBlock(!blocked, id, position);
}
}
});
avatar.setOnClickListener(new View.OnClickListener() {

@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
import android.Manifest;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@ -36,8 +35,10 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -48,6 +49,7 @@ import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.ActionBar;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.Toolbar;
@ -77,9 +79,11 @@ import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Status;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@ -99,8 +103,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
private static final int MEDIA_PICK_RESULT = 1;
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int MEDIA_SIZE_UNKNOWN = -1;
private static final int COMPOSE_SUCCESS = -1;
private static final int THUMBNAIL_SIZE = 128; // pixels
private String inReplyToId;
private EditText textEditor;
@ -120,8 +127,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private TextView charactersLeft;
private Button floatingBtn;
private ImageButton pickBtn;
private ImageButton takeBtn;
private Button nsfwBtn;
private ProgressBar postProgress;
private ImageButton visibilityBtn;
private Uri photoUploadUri;
private static class QueuedMedia {
enum Type {
@ -335,23 +345,17 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
actionBar.setHomeAsUpIndicator(closeIcon);
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
floatingBtn = (Button) findViewById(R.id.floating_btn);
pickBtn = (ImageButton) findViewById(R.id.compose_photo_pick);
takeBtn = (ImageButton) findViewById(R.id.compose_photo_take);
nsfwBtn = (Button) findViewById(R.id.action_toggle_nsfw);
final ImageButton visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
floatingBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pickBtn.setClickable(false);
nsfwBtn.setClickable(false);
visibilityBtn.setClickable(false);
floatingBtn.setEnabled(false);
postProgress.setVisibility(View.VISIBLE);
sendStatus();
}
});
@ -361,6 +365,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
onMediaPick();
}
});
takeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initiateCameraApp();
}
});
nsfwBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -567,26 +577,69 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
}
}
private void disableButtons() {
pickBtn.setClickable(false);
takeBtn.setClickable(false);
nsfwBtn.setClickable(false);
visibilityBtn.setClickable(false);
floatingBtn.setEnabled(false);
}
private void enableButtons() {
pickBtn.setClickable(true);
takeBtn.setClickable(true);
nsfwBtn.setClickable(true);
visibilityBtn.setClickable(true);
floatingBtn.setEnabled(true);
}
private void addLockToSendButton() {
floatingBtn.setText(R.string.action_send);
Drawable lock = AppCompatResources.getDrawable(this, R.drawable.send_private);
if (lock != null) {
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
floatingBtn.setCompoundDrawables(null, null, lock, null);
}
}
private void setStatusVisibility(String visibility) {
statusVisibility = visibility;
switch (visibility) {
case "public": {
floatingBtn.setText(R.string.action_send_public);
floatingBtn.setCompoundDrawables(null, null, null, null);
Drawable globe = AppCompatResources.getDrawable(this, R.drawable.ic_public_24dp);
if (globe != null) {
visibilityBtn.setImageDrawable(globe);
}
break;
}
case "private": {
floatingBtn.setText(R.string.action_send);
Drawable lock = AppCompatResources.getDrawable(this, R.drawable.send_private);
addLockToSendButton();
Drawable lock = AppCompatResources.getDrawable(this,
R.drawable.ic_lock_outline_24dp);
if (lock != null) {
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
floatingBtn.setCompoundDrawables(null, null, lock, null);
visibilityBtn.setImageDrawable(lock);
}
break;
}
case "direct": {
addLockToSendButton();
Drawable envelope = AppCompatResources.getDrawable(this, R.drawable.ic_email_24dp);
if (envelope != null) {
visibilityBtn.setImageDrawable(envelope);
}
break;
}
case "unlisted":
default: {
floatingBtn.setText(R.string.action_send);
floatingBtn.setCompoundDrawables(null, null, null, null);
Drawable openLock = AppCompatResources.getDrawable(this,
R.drawable.ic_lock_open_24dp);
if (openLock != null) {
visibilityBtn.setImageDrawable(openLock);
}
break;
}
}
@ -615,6 +668,18 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
updateVisibleCharactersLeft();
}
void setStateToReadying() {
statusAlreadyInFlight = true;
disableButtons();
postProgress.setVisibility(View.VISIBLE);
}
void setStateToNotReadying() {
postProgress.setVisibility(View.INVISIBLE);
statusAlreadyInFlight = false;
enableButtons();
}
private void sendStatus() {
if (statusAlreadyInFlight) {
return;
@ -624,9 +689,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
if (statusHideText) {
spoilerText = contentWarningEditor.getText().toString();
}
if (contentText.length() + spoilerText.length() <= STATUS_CHARACTER_LIMIT) {
statusAlreadyInFlight = true;
int characterCount = contentText.length() + spoilerText.length();
if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) {
setStateToReadying();
readyStatus(contentText, statusVisibility, statusMarkSensitive, spoilerText);
} else if (characterCount <= 0) {
textEditor.setError(getString(R.string.error_empty));
} else {
textEditor.setError(getString(R.string.error_compose_character_limit));
}
@ -685,11 +753,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
* the status they reply to and that behaviour needs to be kept separate. */
return;
}
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("rememberedVisibility", statusVisibility);
editor.apply();
getPrivatePreferences().edit()
.putString("rememberedVisibility", statusVisibility)
.apply();
}
private EditText createEditText(String[] contentMimeTypes) {
@ -719,7 +785,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
editText.setLayoutParams(layoutParams);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
editText.setEms(10);
editText.setBackgroundColor(0);
editText.setGravity(Gravity.START | Gravity.TOP);
@ -816,13 +882,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private void onSendSuccess() {
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT);
bar.show();
setResult(COMPOSE_SUCCESS);
finish();
}
private void onSendFailure() {
postProgress.setVisibility(View.INVISIBLE);
textEditor.setError(getString(R.string.error_generic));
statusAlreadyInFlight = false;
setStateToNotReadying();
}
private void readyStatus(final String content, final String visibility, final boolean sensitive,
@ -857,7 +923,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
@Override
protected void onCancelled() {
removeAllMediaFromQueue();
statusAlreadyInFlight = false;
setStateToNotReadying();
super.onCancelled();
}
};
@ -882,7 +948,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
readyStatus(content, visibility, sensitive, spoilerText);
}
});
statusAlreadyInFlight = false;
setStateToNotReadying();
}
private void onMediaPick() {
@ -919,6 +985,41 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
}
}
private File createNewImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
String imageFileName = "Tusky_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
return File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
}
private void initiateCameraApp() {
// We don't need to ask for permission in this case, because the used calls require
// android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was
// way before permission dialogues have been introduced.
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createNewImageFile();
} catch (IOException ex) {
displayTransientError(R.string.error_media_upload_opening);
}
// Continue only if the File was successfully created
if (photoFile != null) {
photoUploadUri = FileProvider.getUriForFile(this,
"com.keylesspalace.tusky.fileprovider",
photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri);
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT);
}
}
}
private void initiateMediaPicking() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
@ -932,16 +1033,22 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
startActivityForResult(intent, MEDIA_PICK_RESULT);
}
private void enableMediaPicking() {
private void enableMediaButtons() {
pickBtn.setEnabled(true);
ThemeUtils.setDrawableTint(this, pickBtn.getDrawable(),
R.attr.compose_media_button_tint);
takeBtn.setEnabled(true);
ThemeUtils.setDrawableTint(this, takeBtn.getDrawable(),
R.attr.compose_media_button_tint);
}
private void disableMediaPicking() {
private void disableMediaButtons() {
pickBtn.setEnabled(false);
ThemeUtils.setDrawableTint(this, pickBtn.getDrawable(),
R.attr.compose_media_button_disabled_tint);
takeBtn.setEnabled(false);
ThemeUtils.setDrawableTint(this, takeBtn.getDrawable(),
R.attr.compose_media_button_disabled_tint);
}
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize) {
@ -976,11 +1083,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
textEditor.getPaddingRight(), totalHeight);
// If there's one video in the queue it is full, so disable the button to queue more.
if (item.type == QueuedMedia.Type.VIDEO) {
disableMediaPicking();
disableMediaButtons();
}
} else if (queuedCount >= Status.MAX_MEDIA_ATTACHMENTS) {
// Limit the total media attachments, also.
disableMediaPicking();
disableMediaButtons();
}
if (queuedCount >= 1) {
showMarkSensitive(true);
@ -1003,7 +1110,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
textEditor.getPaddingRight(), 0);
}
enableMediaPicking();
enableMediaButtons();
cancelReadyingMedia(item);
}
@ -1164,6 +1271,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
Uri uri = data.getData();
long mediaSize = getMediaSize(getContentResolver(), uri);
pickMedia(uri, mediaSize);
} else if (requestCode == MEDIA_TAKE_PHOTO_RESULT && resultCode == RESULT_OK) {
long mediaSize = getMediaSize(getContentResolver(), photoUploadUri);
pickMedia(photoUploadUri, mediaSize);
}
}
@ -1190,7 +1300,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(this, uri);
Bitmap source = retriever.getFrameAtTime();
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 128, 128);
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
source.recycle();
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
break;
@ -1203,8 +1313,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
displayTransientError(R.string.error_media_upload_opening);
return;
}
Bitmap source = BitmapFactory.decodeStream(stream);
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 128, 128);
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
source.recycle();
try {
if (stream != null) {

@ -73,10 +73,14 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
radioCheckedId = R.id.radio_unlisted;
}
if (statusVisibility != null) {
if (statusVisibility.equals("unlisted")) {
radioCheckedId = R.id.radio_unlisted;
if (statusVisibility.equals("public")) {
radioCheckedId = R.id.radio_public;
} else if (statusVisibility.equals("private")) {
radioCheckedId = R.id.radio_private;
} else if (statusVisibility.equals("unlisted")) {
radioCheckedId = R.id.radio_unlisted;
} else if (statusVisibility.equals("direct")) {
radioCheckedId = R.id.radio_direct;
}
}
radio.check(radioCheckedId);
@ -113,6 +117,10 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
visibility = "private";
break;
}
case R.id.radio_direct: {
visibility = "direct";
break;
}
}
listener.onVisibilityChanged(visibility);
}

@ -54,7 +54,7 @@ class CustomTabURLSpan extends URLSpan {
customTabsIntent.launchUrl(context, uri);
}
} catch (ActivityNotFoundException e) {
android.util.Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
}
}
}

@ -21,6 +21,7 @@ import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.support.media.ExifInterface;
import java.io.ByteArrayOutputStream;
@ -42,6 +43,7 @@ class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
this.listener = listener;
}
@Nullable
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
Matrix matrix = new Matrix();
switch (orientation) {

@ -0,0 +1,515 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.Base64;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Profile;
import com.theartofdev.edmodo.cropper.CropImage;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class EditProfileActivity extends BaseActivity {
private static final String TAG = "EditProfileActivity";
private static final int AVATAR_PICK_RESULT = 1;
private static final int HEADER_PICK_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int AVATAR_WIDTH = 120;
private static final int AVATAR_HEIGHT = 120;
private static final int HEADER_WIDTH = 700;
private static final int HEADER_HEIGHT = 335;
private enum PickType {
NOTHING,
AVATAR,
HEADER
}
@BindView(R.id.edit_profile_display_name) EditText displayNameEditText;
@BindView(R.id.edit_profile_note) EditText noteEditText;
@BindView(R.id.edit_profile_avatar) Button avatarButton;
@BindView(R.id.edit_profile_avatar_preview) ImageView avatarPreview;
@BindView(R.id.edit_profile_avatar_progress) ProgressBar avatarProgress;
@BindView(R.id.edit_profile_header) Button headerButton;
@BindView(R.id.edit_profile_header_preview) ImageView headerPreview;
@BindView(R.id.edit_profile_header_progress) ProgressBar headerProgress;
@BindView(R.id.edit_profile_save_progress) ProgressBar saveProgress;
private String priorDisplayName;
private String priorNote;
private boolean isAlreadySaving;
private PickType currentlyPicking;
private String avatarBase64;
private String headerBase64;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_profile);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(null);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
if (savedInstanceState != null) {
priorDisplayName = savedInstanceState.getString("priorDisplayName");
priorNote = savedInstanceState.getString("priorNote");
isAlreadySaving = savedInstanceState.getBoolean("isAlreadySaving");
currentlyPicking = (PickType) savedInstanceState.getSerializable("currentlyPicking");
avatarBase64 = savedInstanceState.getString("avatarBase64");
headerBase64 = savedInstanceState.getString("headerBase64");
} else {
priorDisplayName = null;
priorNote = null;
isAlreadySaving = false;
currentlyPicking = PickType.NOTHING;
avatarBase64 = null;
headerBase64 = null;
}
avatarButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onMediaPick(PickType.AVATAR);
}
});
headerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onMediaPick(PickType.HEADER);
}
});
avatarPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
avatarPreview.setImageBitmap(null);
avatarPreview.setVisibility(View.GONE);
avatarBase64 = null;
}
});
headerPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
headerPreview.setImageBitmap(null);
headerPreview.setVisibility(View.GONE);
headerBase64 = null;
}
});
mastodonAPI.accountVerifyCredentials().enqueue(new Callback<Account>() {
@Override
public void onResponse(Call<Account> call, Response<Account> response) {
if (!response.isSuccessful()) {
onAccountVerifyCredentialsFailed();
return;
}
Account me = response.body();
priorDisplayName = me.getDisplayName();
priorNote = me.note.toString();
displayNameEditText.setText(priorDisplayName);
noteEditText.setText(priorNote);
}
@Override
public void onFailure(Call<Account> call, Throwable t) {
onAccountVerifyCredentialsFailed();
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("priorDisplayName", priorDisplayName);
outState.putString("priorNote", priorNote);
outState.putBoolean("isAlreadySaving", isAlreadySaving);
outState.putSerializable("currentlyPicking", currentlyPicking);
outState.putString("avatarBase64", avatarBase64);
outState.putString("headerBase64", headerBase64);
super.onSaveInstanceState(outState);
}
private void onAccountVerifyCredentialsFailed() {
Log.e(TAG, "The account failed to load.");
}
private void onMediaPick(PickType pickType) {
if (currentlyPicking != PickType.NOTHING) {
// Ignore inputs if another pick operation is still occurring.
return;
}
currentlyPicking = pickType;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
} else {
initiateMediaPicking();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initiateMediaPicking();
} else {
endMediaPicking();
Snackbar.make(avatarButton, R.string.error_media_upload_permission,
Snackbar.LENGTH_LONG).show();
}
break;
}
}
}
private void initiateMediaPicking() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
switch (currentlyPicking) {
case AVATAR: { startActivityForResult(intent, AVATAR_PICK_RESULT); break; }
case HEADER: { startActivityForResult(intent, HEADER_PICK_RESULT); break; }
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.edit_profile_toolbar, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case R.id.action_save: {
save();
return true;
}
}
return super.onOptionsItemSelected(item);
}
private void save() {
if (isAlreadySaving || currentlyPicking != PickType.NOTHING) {
return;
}
String newDisplayName = displayNameEditText.getText().toString();
if (newDisplayName.isEmpty()) {
displayNameEditText.setError(getString(R.string.error_empty));
return;
}
if (priorDisplayName != null && priorDisplayName.equals(newDisplayName)) {
// If it's not any different, don't patch it.
newDisplayName = null;
}
String newNote = noteEditText.getText().toString();
if (newNote.isEmpty()) {
noteEditText.setError(getString(R.string.error_empty));
return;
}
if (priorNote != null && priorNote.equals(newNote)) {
// If it's not any different, don't patch it.
newNote = null;
}
if (newDisplayName == null && newNote == null && avatarBase64 == null
&& headerBase64 == null) {
// If nothing is changed, then there's nothing to save.
return;
}
saveProgress.setVisibility(View.VISIBLE);
isAlreadySaving = true;
Profile profile = new Profile();
profile.displayName = newDisplayName;
profile.note = newNote;
profile.avatar = avatarBase64;
profile.header = headerBase64;
mastodonAPI.accountUpdateCredentials(profile).enqueue(new Callback<Account>() {
@Override
public void onResponse(Call<Account> call, Response<Account> response) {
if (!response.isSuccessful()) {
onSaveFailure();
return;
}
getPrivatePreferences().edit()
.putBoolean("refreshProfileHeader", true)
.apply();
finish();
}
@Override
public void onFailure(Call<Account> call, Throwable t) {
onSaveFailure();
}
});
}
private void onSaveFailure() {
isAlreadySaving = false;
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
.show();
saveProgress.setVisibility(View.GONE);
}
private void beginMediaPicking() {
switch (currentlyPicking) {
case AVATAR: {
avatarProgress.setVisibility(View.VISIBLE);
avatarPreview.setVisibility(View.INVISIBLE);
break;
}
case HEADER: {
headerProgress.setVisibility(View.VISIBLE);
headerPreview.setVisibility(View.INVISIBLE);
break;
}
}
}
private void endMediaPicking() {
switch (currentlyPicking) {
case AVATAR: {
avatarProgress.setVisibility(View.GONE);
avatarPreview.setVisibility(View.GONE);
break;
}
case HEADER: {
headerProgress.setVisibility(View.GONE);
headerPreview.setVisibility(View.GONE);
break;
}
}
currentlyPicking = PickType.NOTHING;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case AVATAR_PICK_RESULT: {
if (resultCode == RESULT_OK && data != null) {
CropImage.activity(data.getData())
.setInitialCropWindowPaddingRatio(0)
.setAspectRatio(AVATAR_WIDTH, AVATAR_HEIGHT)
.start(this);
} else {
endMediaPicking();
}
break;
}
case HEADER_PICK_RESULT: {
if (resultCode == RESULT_OK && data != null) {
CropImage.activity(data.getData())
.setInitialCropWindowPaddingRatio(0)
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
.start(this);
} else {
endMediaPicking();
}
break;
}
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: {
CropImage.ActivityResult result = CropImage.getActivityResult(data);
if (resultCode == RESULT_OK) {
beginResize(result.getUri());
} else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
onResizeFailure();
}
break;
}
}
}
private void beginResize(Uri uri) {
beginMediaPicking();
int width, height;
switch (currentlyPicking) {
default: {
throw new AssertionError("PickType not set.");
}
case AVATAR: {
width = AVATAR_WIDTH;
height = AVATAR_HEIGHT;
break;
}
case HEADER: {
width = HEADER_WIDTH;
height = HEADER_HEIGHT;
break;
}
}
new ResizeImageTask(getContentResolver(), width, height, new ResizeImageTask.Listener() {
@Override
public void onSuccess(List<Bitmap> contentList) {
Bitmap bitmap = contentList.get(0);
PickType pickType = currentlyPicking;
endMediaPicking();
switch (pickType) {
case AVATAR: {
avatarPreview.setImageBitmap(bitmap);
avatarPreview.setVisibility(View.VISIBLE);
avatarBase64 = bitmapToBase64(bitmap);
break;
}
case HEADER: {
headerPreview.setImageBitmap(bitmap);
headerPreview.setVisibility(View.VISIBLE);
headerBase64 = bitmapToBase64(bitmap);
break;
}
}
}
@Override
public void onFailure() {
onResizeFailure();
}
}).execute(uri);
}
private void onResizeFailure() {
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
.show();
endMediaPicking();
}
private static String bitmapToBase64(Bitmap bitmap) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArray = stream.toByteArray();
IOUtils.closeQuietly(stream);
return "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.DEFAULT);
}
private static class ResizeImageTask extends AsyncTask<Uri, Void, Boolean> {
private ContentResolver contentResolver;
private int resizeWidth;
private int resizeHeight;
private Listener listener;
private List<Bitmap> resultList;
ResizeImageTask(ContentResolver contentResolver, int width, int height, Listener listener) {
this.contentResolver = contentResolver;
this.resizeWidth = width;
this.resizeHeight = height;
this.listener = listener;
}
@Override
protected Boolean doInBackground(Uri... uris) {
resultList = new ArrayList<>();
for (Uri uri : uris) {
InputStream inputStream;
try {
inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) {
return false;
}
Bitmap sourceBitmap;
try {
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null);
} catch (OutOfMemoryError error) {
return false;
} finally {
IOUtils.closeQuietly(inputStream);
}
if (sourceBitmap == null) {
return false;
}
Bitmap bitmap = Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight,
false);
sourceBitmap.recycle();
if (bitmap == null) {
return false;
}
resultList.add(bitmap);
if (isCancelled()) {
return false;
}
}
return true;
}
@Override
protected void onPostExecute(Boolean successful) {
if (successful) {
listener.onSuccess(resultList);
} else {
listener.onFailure();
}
super.onPostExecute(successful);
}
interface Listener {
void onSuccess(List<Bitmap> contentList);
void onFailure();
}
}
}

@ -0,0 +1,129 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Account;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
import butterknife.BindView;
import butterknife.ButterKnife;
class FollowRequestsAdapter extends AccountAdapter {
private static final int VIEW_TYPE_FOLLOW_REQUEST = 0;
private static final int VIEW_TYPE_FOOTER = 1;
FollowRequestsAdapter(AccountActionListener accountActionListener) {
super(accountActionListener);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
default:
case VIEW_TYPE_FOLLOW_REQUEST: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view);
}
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener);
}
}
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_FOLLOW_REQUEST;
}
}
static class FollowRequestViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.follow_request_avatar) CircularImageView avatar;
@BindView(R.id.follow_request_username) TextView username;
@BindView(R.id.follow_request_display_name) TextView displayName;
@BindView(R.id.follow_request_accept) ImageButton accept;
@BindView(R.id.follow_request_reject) ImageButton reject;
private String id;
FollowRequestViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void setupWithAccount(Account account) {
id = account.id;
displayName.setText(account.getDisplayName());
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
Picasso.with(avatar.getContext())
.load(account.avatar)
.error(R.drawable.avatar_error)
.placeholder(R.drawable.avatar_default)
.into(avatar);
}
void setupActionListener(final AccountActionListener listener) {
accept.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position);
}
}
});
reject.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(false, id, position);
}
}
});
avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewAccount(id);
}
});
}
}
}

@ -23,6 +23,8 @@ class FooterViewHolder extends RecyclerView.ViewHolder {
FooterViewHolder(View itemView) {
super(itemView);
ProgressBar progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
progressBar.setIndeterminate(true);
if (progressBar != null) {
progressBar.setIndeterminate(true);
}
}
}

@ -19,6 +19,7 @@ import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class IOUtils {
static void closeQuietly(@Nullable InputStream stream) {
@ -30,4 +31,14 @@ class IOUtils {
// intentionally unhandled
}
}
static void closeQuietly(@Nullable OutputStream stream) {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
// intentionally unhandled
}
}
}

@ -0,0 +1,82 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Status;
class LinkHelper {
static void setClickableText(TextView view, Spanned content,
@Nullable Status.Mention[] mentions,
final LinkListener listener) {
SpannableStringBuilder builder = new SpannableStringBuilder(content);
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(view.getContext())
.getBoolean("customTabs", true);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence text = builder.subSequence(start, end);
if (text.charAt(0) == '#') {
final String tag = text.subSequence(1, text.length()).toString();
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewTag(tag);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
} else if (text.charAt(0) == '@' && mentions != null) {
final String accountUsername = text.subSequence(1, text.length()).toString();
String id = null;
for (Status.Mention mention : mentions) {
if (mention.localUsername.equals(accountUsername)) {
id = mention.id;
}
}
if (id != null) {
final String accountId = id;
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewAccount(accountId);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
} else if (useCustomTabs) {
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
}
view.setText(builder);
view.setLinksClickable(true);
view.setMovementMethod(LinkMovementMethod.getInstance());
}
}

@ -0,0 +1,21 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
interface LinkListener {
void onViewTag(String tag);
void onViewAccount(String id);
}

@ -16,15 +16,17 @@
package com.keylesspalace.tusky;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod;
import android.view.View;
@ -110,26 +112,6 @@ public class LoginActivity extends AppCompatActivity {
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
});
// Apply any updates needed.
int versionCode = 1;
try {
versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage());
}
if (preferences.getInt("lastUpdateVersion", 0) != versionCode) {
SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI.
* But to fix it requires forcing the app to re-authenticate with servers. So, clear
* out the stored client id/secret pairs. The only other things that are lost are
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear();
}
editor.putInt("lastUpdateVersion", versionCode);
editor.apply();
}
}
@Override
@ -201,10 +183,10 @@ public class LoginActivity extends AppCompatActivity {
AppCredentials credentials = response.body();
clientId = credentials.clientId;
clientSecret = credentials.clientSecret;
SharedPreferences.Editor editor = preferences.edit();
editor.putString(domain + "/client_id", clientId);
editor.putString(domain + "/client_secret", clientSecret);
editor.apply();
preferences.edit()
.putString(domain + "/client_id", clientId)
.putString(domain + "/client_secret", clientSecret)
.apply();
redirectUserToAuthorizeAndLogin(editText);
}
@ -226,7 +208,6 @@ public class LoginActivity extends AppCompatActivity {
}
}
/**
* Chain together the key-value pairs into a query string, for either appending to a URL or
* as the content of an HTTP request.
@ -245,6 +226,36 @@ public class LoginActivity extends AppCompatActivity {
return s.toString();
}
private static boolean openInCustomTab(Uri uri, Context context) {
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("lightTheme", false);
int toolbarColorRes;
if (lightTheme) {
toolbarColorRes = R.color.custom_tab_toolbar_light;
} else {
toolbarColorRes = R.color.custom_tab_toolbar_dark;
}
int toolbarColor = ContextCompat.getColor(context, toolbarColorRes);
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(toolbarColor);
CustomTabsIntent customTabsIntent = builder.build();
try {
String packageName = CustomTabsHelper.getPackageNameToUse(context);
/* If we cant find a package name, it means theres no browser that supports
* Chrome Custom Tabs installed. So, we fallback to the webview */
if (packageName == null) {
return false;
} else {
customTabsIntent.intent.setPackage(packageName);
customTabsIntent.launchUrl(context, uri);
}
} catch (ActivityNotFoundException e) {
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
return false;
}
return true;
}
private void redirectUserToAuthorizeAndLogin(EditText editText) {
/* To authorize this app and log in it's necessary to redirect to the domain given,
* activity_login there, and the server will redirect back to the app with its response. */
@ -256,11 +267,14 @@ public class LoginActivity extends AppCompatActivity {
parameters.put("response_type", "code");
parameters.put("scope", OAUTH_SCOPES);
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (viewIntent.resolveActivity(getPackageManager()) != null) {
startActivity(viewIntent);
} else {
editText.setError(getString(R.string.error_no_web_browser_found));
Uri uri = Uri.parse(url);
if (!openInCustomTab(uri, this)) {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri);
if (viewIntent.resolveActivity(getPackageManager()) != null) {
startActivity(viewIntent);
} else {
editText.setError(getString(R.string.error_no_web_browser_found));
}
}
}
@ -268,11 +282,11 @@ public class LoginActivity extends AppCompatActivity {
protected void onStop() {
super.onStop();
if (domain != null) {
SharedPreferences.Editor editor = preferences.edit();
editor.putString("domain", domain);
editor.putString("clientId", clientId);
editor.putString("clientSecret", clientSecret);
editor.apply();
preferences.edit()
.putString("domain", domain)
.putString("clientId", clientId)
.putString("clientSecret", clientSecret)
.apply();
}
}
@ -347,10 +361,14 @@ public class LoginActivity extends AppCompatActivity {
}
private void onLoginSuccess(String accessToken) {
SharedPreferences.Editor editor = preferences.edit();
editor.putString("domain", domain);
editor.putString("accessToken", accessToken);
editor.commit();
boolean committed = preferences.edit()
.putString("domain", domain)
.putString("accessToken", accessToken)
.commit();
if (!committed) {
editText.setError(getString(R.string.error_retrieving_oauth_token));
return;
}
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();

@ -16,7 +16,6 @@
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
@ -24,8 +23,11 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@ -64,8 +66,9 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity"; // logging tag and Volley request tag
public class MainActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
private static final String TAG = "MainActivity"; // logging tag
protected static int COMPOSE_RESULT = 1;
private String loggedInAccountId;
private String loggedInAccountUsername;
@ -99,7 +102,7 @@ public class MainActivity extends BaseActivity {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), ComposeActivity.class);
startActivity(intent);
startActivityForResult(intent, COMPOSE_RESULT);
}
});
@ -184,7 +187,11 @@ public class MainActivity extends BaseActivity {
}
// Setup push notifications
if (arePushNotificationsEnabled()) enablePushNotifications();
if (arePushNotificationsEnabled()) {
enablePushNotifications();
} else {
disablePushNotifications();
}
composeButton = floatingBtn;
}
@ -193,12 +200,24 @@ public class MainActivity extends BaseActivity {
protected void onResume() {
super.onResume();
SharedPreferences notificationPreferences = getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
SharedPreferences.Editor editor = notificationPreferences.edit();
editor.putString("current", "[]");
editor.apply();
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).cancel(MyFirebaseMessagingService.NOTIFY_ID);
SharedPreferences notificationPreferences = getApplicationContext()
.getSharedPreferences("Notifications", MODE_PRIVATE);
notificationPreferences.edit()
.putString("current", "[]")
.apply();
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE)))
.cancel(MessagingService.NOTIFY_ID);
/* After editing a profile, the profile header in the navigation drawer needs to be
* refreshed */
SharedPreferences preferences = getPrivatePreferences();
if (preferences.getBoolean("refreshProfileHeader", false)) {
fetchUserInfo();
preferences.edit()
.putBoolean("refreshProfileHeader", false)
.apply();
}
}
@Override
@ -254,6 +273,9 @@ public class MainActivity extends BaseActivity {
}
});
Drawable muteDrawable = ContextCompat.getDrawable(this, R.drawable.ic_mute_24dp);
ThemeUtils.setDrawableTint(this, muteDrawable, R.attr.toolbar_icon_tint);
drawer = new DrawerBuilder()
.withActivity(this)
//.withToolbar(toolbar)
@ -261,12 +283,13 @@ public class MainActivity extends BaseActivity {
.withHasStableIds(true)
.withSelectedItem(-1)
.addDrawerItems(
new PrimaryDrawerItem().withIdentifier(0).withName(R.string.action_view_profile).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person),
new PrimaryDrawerItem().withIdentifier(0).withName(getString(R.string.action_edit_profile)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person),
new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star),
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block),
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable),
new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block),
new DividerDrawerItem(),
new SecondaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_preferences)).withSelectable(false),
new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_logout)).withSelectable(false)
new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings),
new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)
)
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
@Override
@ -275,22 +298,28 @@ public class MainActivity extends BaseActivity {
long drawerItemIdentifier = drawerItem.getIdentifier();
if (drawerItemIdentifier == 0) {
if (loggedInAccountId != null) {
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
intent.putExtra("id", loggedInAccountId);
startActivity(intent);
}
Intent intent = new Intent(MainActivity.this, EditProfileActivity.class);
startActivity(intent);
} else 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);
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
intent.putExtra("type", AccountListActivity.Type.MUTES);
startActivity(intent);
} else if (drawerItemIdentifier == 3) {
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
intent.putExtra("type", AccountListActivity.Type.BLOCKS);
startActivity(intent);
} else if (drawerItemIdentifier == 4) {
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 5) {
logout();
} else if (drawerItemIdentifier == 6) {
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS);
startActivity(intent);
}
}
@ -303,11 +332,10 @@ public class MainActivity extends BaseActivity {
private void logout() {
if (arePushNotificationsEnabled()) disablePushNotifications();
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();
getPrivatePreferences().edit()
.remove("domain")
.remove("accessToken")
.apply();
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
@ -377,9 +405,7 @@ public class MainActivity extends BaseActivity {
}
@Override
public void onSearchAction(String currentQuery) {
}
public void onSearchAction(String currentQuery) {}
});
searchView.setOnBindSuggestionCallback(new SearchSuggestionsAdapter.OnBindSuggestionCallback() {
@ -404,8 +430,7 @@ public class MainActivity extends BaseActivity {
}
private void fetchUserInfo() {
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
final String domain = preferences.getString("domain", null);
String id = preferences.getString("loggedInAccountId", null);
String username = preferences.getString("loggedInAccountUsername", null);
@ -422,34 +447,7 @@ public class MainActivity extends BaseActivity {
onFetchUserInfoFailure(new Exception(response.message()));
return;
}
Account me = response.body();
ImageView background = headerResult.getHeaderBackgroundView();
int backgroundWidth = background.getWidth();
int backgroundHeight = background.getHeight();
if (backgroundWidth == 0 || backgroundHeight == 0) {
/* The header ImageView may not be layed out when the verify credentials call
* returns so measure the dimensions and use those. */
background.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY);
backgroundWidth = background.getMeasuredWidth();
backgroundHeight = background.getMeasuredHeight();
}
Picasso.with(MainActivity.this)
.load(me.header)
.placeholder(R.drawable.account_header_missing)
.resize(backgroundWidth, backgroundHeight)
.centerCrop()
.into(background);
headerResult.addProfiles(
new ProfileDrawerItem()
.withName(me.getDisplayName())
.withEmail(String.format("%s@%s", me.username, domain))
.withIcon(me.avatar)
);
onFetchUserInfoSuccess(me.id, me.username);
onFetchUserInfoSuccess(response.body(), domain);
}
@Override
@ -459,21 +457,69 @@ public class MainActivity extends BaseActivity {
});
}
private void onFetchUserInfoSuccess(String id, String username) {
loggedInAccountId = id;
loggedInAccountUsername = username;
SharedPreferences preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("loggedInAccountId", loggedInAccountId);
editor.putString("loggedInAccountUsername", loggedInAccountUsername);
editor.apply();
private void onFetchUserInfoSuccess(Account me, String domain) {
// Add the header image and avatar from the account, into the navigation drawer header.
headerResult.clear();
ImageView background = headerResult.getHeaderBackgroundView();
int backgroundWidth = background.getWidth();
int backgroundHeight = background.getHeight();
if (backgroundWidth == 0 || backgroundHeight == 0) {
/* The header ImageView may not be layed out when the verify credentials call returns so
* measure the dimensions and use those. */
background.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY);
backgroundWidth = background.getMeasuredWidth();
backgroundHeight = background.getMeasuredHeight();
}
Picasso.with(MainActivity.this)
.load(me.header)
.placeholder(R.drawable.account_header_missing)
.resize(backgroundWidth, backgroundHeight)
.centerCrop()
.into(background);
headerResult.addProfiles(
new ProfileDrawerItem()
.withName(me.getDisplayName())
.withEmail(String.format("%s@%s", me.username, domain))
.withIcon(me.avatar)
);
// Show follow requests in the menu, if this is a locked account.
if (me.locked) {
PrimaryDrawerItem followRequestsItem = new PrimaryDrawerItem()
.withIdentifier(6)
.withName(R.string.action_view_follow_requests)
.withSelectable(false)
.withIcon(GoogleMaterial.Icon.gmd_person_add);
drawer.addItemAtPosition(followRequestsItem, 3);
}
// Update the current login information.
loggedInAccountId = me.id;
loggedInAccountUsername = me.username;
getPrivatePreferences().edit()
.putString("loggedInAccountId", loggedInAccountId)
.putString("loggedInAccountUsername", loggedInAccountUsername)
.apply();
}
private void onFetchUserInfoFailure(Exception exception) {
Log.e(TAG, "Failed to fetch user info. " + exception.getMessage());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
if (adapter.getCurrentFragment() instanceof SFragment) {
((SFragment) adapter.getCurrentFragment()).onSuccessfulStatus();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
if(drawer != null && drawer.isDrawerOpen()) {
@ -485,4 +531,25 @@ public class MainActivity extends BaseActivity {
viewPager.setCurrentItem(pageHistory.peek());
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
for (Fragment fragment : adapter.getRegisteredFragments()) {
fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override
public void onUserRemoved(String accountId) {
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
for (Fragment fragment : adapter.getRegisteredFragments()) {
if (fragment instanceof StatusRemoveListener) {
StatusRemoveListener listener = (StatusRemoveListener) fragment;
listener.removePostsByUser(accountId);
}
}
}
}

@ -20,6 +20,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.AppCredentials;
import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
@ -29,6 +30,7 @@ import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
@ -115,11 +117,7 @@ public interface MastodonAPI {
@GET("api/v1/accounts/verify_credentials")
Call<Account> accountVerifyCredentials();
@PATCH("api/v1/accounts/update_credentials")
Call<Account> accountUpdateCredentials(
@Field("display_name") String displayName,
@Field("note") String note,
@Field("avatar") String avatar,
@Field("header") String header);
Call<Account> accountUpdateCredentials(@Body Profile profile);
@GET("api/v1/accounts/search")
Call<List<Account>> searchAccounts(
@Query("q") String q,

@ -0,0 +1,105 @@
package com.keylesspalace.tusky;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Account;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
import java.util.HashSet;
import java.util.Set;
import butterknife.BindView;
import butterknife.ButterKnife;
class MutesAdapter extends AccountAdapter {
private static final int VIEW_TYPE_MUTED_USER = 0;
private static final int VIEW_TYPE_FOOTER = 1;
MutesAdapter(AccountActionListener accountActionListener) {
super(accountActionListener);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
default:
case VIEW_TYPE_MUTED_USER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_muted_user, parent, false);
return new MutesAdapter.MutedUserViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view);
}
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener, true, position);
}
}
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_MUTED_USER;
}
}
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.muted_user_avatar) CircularImageView avatar;
@BindView(R.id.muted_user_username) TextView username;
@BindView(R.id.muted_user_display_name) TextView displayName;
@BindView(R.id.muted_user_unmute) ImageButton unmute;
private String id;
MutedUserViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void setupWithAccount(Account account) {
id = account.id;
displayName.setText(account.getDisplayName());
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
Picasso.with(avatar.getContext())
.load(account.avatar)
.error(R.drawable.avatar_error)
.placeholder(R.drawable.avatar_default)
.into(avatar);
}
void setupActionListener(final AccountActionListener listener, final boolean muted,
final int position) {
unmute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onMute(!muted, id, position);
}
});
avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewAccount(id);
}
});
}
}
}

@ -1,318 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
* additional permission to convey the resulting work. */
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.Spanned;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.Notification;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private MastodonAPI mastodonAPI;
private static final String TAG = "MyFirebaseMessagingService";
public static final int NOTIFY_ID = 666;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, remoteMessage.getFrom());
Log.d(TAG, remoteMessage.toString());
String notificationId = remoteMessage.getData().get("notification_id");
if (notificationId == null) {
Log.e(TAG, "No notification ID in payload!!");
return;
}
Log.d(TAG, notificationId);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
boolean enabled = preferences.getBoolean("notificationsEnabled", true);
if (!enabled) {
return;
}
createMastodonAPI();
mastodonAPI.notification(notificationId).enqueue(new Callback<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
if (response.isSuccessful()) {
buildNotification(response.body());
}
}
@Override
public void onFailure(Call<Notification> call, Throwable t) {
}
});
}
private void createMastodonAPI() {
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null);
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
@Override
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);
}
})
.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 = retrofit.create(MastodonAPI.class);
}
private String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private static boolean filterNotification(SharedPreferences preferences,
Notification notification) {
switch (notification.type) {
default:
case MENTION: {
return preferences.getBoolean("notificationFilterMentions", true);
}
case FOLLOW: {
return preferences.getBoolean("notificationFilterFollows", true);
}
case REBLOG: {
return preferences.getBoolean("notificationFilterReblogs", true);
}
case FAVOURITE: {
return preferences.getBoolean("notificationFilterFavourites", true);
}
}
}
private void buildNotification(Notification body) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final SharedPreferences notificationPreferences = getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
if (!filterNotification(preferences, body)) {
return;
}
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
JSONArray currentNotifications;
try {
currentNotifications = new JSONArray(rawCurrentNotifications);
} catch (JSONException e) {
currentNotifications = new JSONArray();
}
boolean alreadyContains = false;
for(int i = 0; i < currentNotifications.length(); i++) {
try {
if (currentNotifications.getString(i).equals(body.account.displayName)) {
alreadyContains = true;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!alreadyContains) {
currentNotifications.put(body.account.displayName);
}
SharedPreferences.Editor editor = notificationPreferences.edit();
editor.putString("current", currentNotifications.toString());
editor.commit();
Intent resultIntent = new Intent(this, MainActivity.class);
resultIntent.putExtra("tab_position", 1);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Intent deleteIntent = new Intent(this, NotificationClearBroadcastReceiver.class);
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_notify)
.setContentIntent(resultPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
if (currentNotifications.length() == 1) {
builder.setContentTitle(titleForType(body))
.setContentText(truncateWithEllipses(bodyForType(body), 40));
Target mTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
builder.setLargeIcon(bitmap);
setupPreferences(preferences, builder);
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
Picasso.with(this)
.load(body.account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(mTarget);
} else {
setupPreferences(preferences, builder);
try {
builder.setContentTitle(String.format(getString(R.string.notification_title_summary), currentNotifications.length()))
.setContentText(truncateWithEllipses(joinNames(currentNotifications), 40));
} catch (JSONException e) {
e.printStackTrace();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
}
private void setupPreferences(SharedPreferences preferences, NotificationCompat.Builder builder) {
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationAlertVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationAlertLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
}
private String joinNames(JSONArray array) throws JSONException {
if (array.length() > 3) {
return String.format(getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
} else if (array.length() == 3) {
return String.format(getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
} else if (array.length() == 2) {
return String.format(getString(R.string.notification_summary_small), array.get(0), array.get(1));
}
return null;
}
private String titleForType(Notification notification) {
switch (notification.type) {
case MENTION:
return String.format(getString(R.string.notification_mention_format), notification.account.getDisplayName());
case FOLLOW:
return String.format(getString(R.string.notification_follow_format), notification.account.getDisplayName());
case FAVOURITE:
return String.format(getString(R.string.notification_favourite_format), notification.account.getDisplayName());
case REBLOG:
return String.format(getString(R.string.notification_reblog_format), notification.account.getDisplayName());
}
return null;
}
private String bodyForType(Notification notification) {
switch (notification.type) {
case FOLLOW:
return notification.account.username;
case MENTION:
case FAVOURITE:
case REBLOG:
return notification.status.content.toString();
}
return null;
}
}

@ -0,0 +1,224 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import com.keylesspalace.tusky.entity.Notification;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.json.JSONArray;
import org.json.JSONException;
class NotificationMaker {
static void make(final Context context, final int notifyId, Notification body) {
final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context);
final SharedPreferences notificationPreferences = context.getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
if (!filterNotification(preferences, body)) {
return;
}
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
JSONArray currentNotifications;
try {
currentNotifications = new JSONArray(rawCurrentNotifications);
} catch (JSONException e) {
currentNotifications = new JSONArray();
}
boolean alreadyContains = false;
for(int i = 0; i < currentNotifications.length(); i++) {
try {
if (currentNotifications.getString(i).equals(body.account.getDisplayName())) {
alreadyContains = true;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!alreadyContains) {
currentNotifications.put(body.account.getDisplayName());
}
notificationPreferences.edit()
.putString("current", currentNotifications.toString())
.commit();
Intent resultIntent = new Intent(context, MainActivity.class);
resultIntent.putExtra("tab_position", 1);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notify)
.setContentIntent(resultPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
if (currentNotifications.length() == 1) {
builder.setContentTitle(titleForType(context, body))
.setContentText(truncateWithEllipses(bodyForType(body), 40));
Target mTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
builder.setLargeIcon(bitmap);
setupPreferences(preferences, builder);
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
.notify(notifyId, builder.build());
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {}
};
Picasso.with(context)
.load(body.account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(mTarget);
} else {
setupPreferences(preferences, builder);
try {
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
} catch (JSONException e) {
e.printStackTrace();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
.notify(notifyId, builder.build());
}
private static boolean filterNotification(SharedPreferences preferences,
Notification notification) {
switch (notification.type) {
default:
case MENTION: {
return preferences.getBoolean("notificationFilterMentions", true);
}
case FOLLOW: {
return preferences.getBoolean("notificationFilterFollows", true);
}
case REBLOG: {
return preferences.getBoolean("notificationFilterReblogs", true);
}
case FAVOURITE: {
return preferences.getBoolean("notificationFilterFavourites", true);
}
}
}
private static String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private static void setupPreferences(SharedPreferences preferences,
NotificationCompat.Builder builder) {
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationAlertVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationAlertLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
}
@Nullable
private static String joinNames(Context context, JSONArray array) throws JSONException {
if (array.length() > 3) {
return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
} else if (array.length() == 3) {
return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
} else if (array.length() == 2) {
return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1));
}
return null;
}
@Nullable
private static String titleForType(Context context, Notification notification) {
switch (notification.type) {
case MENTION:
return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName());
case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName());
case FAVOURITE:
return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName());
case REBLOG:
return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName());
}
return null;
}
@Nullable
private static String bodyForType(Notification notification) {
switch (notification.type) {
case FOLLOW:
return notification.account.username;
case MENTION:
case FAVOURITE:
case REBLOG:
return notification.status.content.toString();
}
return null;
}
}

@ -34,6 +34,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.squareup.picasso.Picasso;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
@ -42,9 +43,16 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
private static final int VIEW_TYPE_FOLLOW = 3;
enum FooterState {
EMPTY,
END,
LOADING
}
private List<Notification> notifications;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private FooterState footerState = FooterState.END;
NotificationsAdapter(StatusActionListener statusListener,
NotificationActionListener notificationActionListener) {
@ -54,6 +62,15 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
this.notificationActionListener = notificationActionListener;
}
void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(notifications.size());
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
@ -64,8 +81,24 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
break;
case END: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_end, parent, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_empty, parent, false);
break;
}
}
return new FooterViewHolder(view);
}
case VIEW_TYPE_STATUS_NOTIFICATION: {
@ -178,6 +211,18 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
notifyItemChanged(position);
}
public void removeAllByAccountId(String id) {
for (int i = 0; i < notifications.size();) {
Notification notification = notifications.get(i);
if (id.equals(notification.account.id)) {
notifications.remove(i);
notifyItemRemoved(i);
} else {
i += 1;
}
}
}
interface NotificationActionListener {
void onViewAccount(String id);
}

@ -19,7 +19,9 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration;
@ -39,16 +41,18 @@ import retrofit2.Callback;
import retrofit2.Response;
public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
NotificationsAdapter.NotificationActionListener {
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener,
NotificationsAdapter.NotificationActionListener, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "Notifications"; // logging tag
private SwipeRefreshLayout swipeRefreshLayout;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
private EndlessOnScrollListener scrollListener;
private NotificationsAdapter adapter;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
private Call<List<Notification>> listCall;
private boolean hideFab;
public static NotificationsFragment newInstance() {
NotificationsFragment fragment = new NotificationsFragment();
@ -57,15 +61,10 @@ public class NotificationsFragment extends SFragment implements
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
// Setup the SwipeRefreshLayout.
@ -73,7 +72,7 @@ public class NotificationsFragment extends SFragment implements
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
swipeRefreshLayout.setOnRefreshListener(this);
// Setup the RecyclerView.
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
@ -83,19 +82,7 @@ public class NotificationsFragment extends SFragment implements
R.drawable.status_divider_dark);
divider.setDrawable(drawable);
recyclerView.addItemDecoration(divider);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
if (notification != null) {
sendFetchNotificationsRequest(notification.id, null);
} else {
sendFetchNotificationsRequest();
}
}
};
recyclerView.addOnScrollListener(scrollListener);
adapter = new NotificationsAdapter(this, this);
recyclerView.setAdapter(adapter);
@ -118,9 +105,48 @@ public class NotificationsFragment extends SFragment implements
}
@Override
public void onResume() {
super.onResume();
sendFetchNotificationsRequest();
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then.
* Use a modified scroll listener that both loads more notifications as it goes, and hides
* the compose button on down-scroll. */
MainActivity activity = (MainActivity) getActivity();
final FloatingActionButton composeButton = activity.composeButton;
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
activity);
preferences.registerOnSharedPreferenceChangeListener(this);
hideFab = preferences.getBoolean("fabHide", false);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
super.onScrolled(view, dx, dy);
if (hideFab) {
if (dy > 0 && composeButton.isShown()) {
composeButton.hide(); // hides the button if we're scrolling down
} else if (dy < 0 && !composeButton.isShown()) {
composeButton.show(); // shows it if we are scrolling up
}
} else if (!composeButton.isShown()) {
composeButton.show();
}
}
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
if (notification != null) {
sendFetchNotificationsRequest(notification.id, null);
} else {
sendFetchNotificationsRequest();
}
}
};
recyclerView.addOnScrollListener(scrollListener);
}
@Override
@ -142,9 +168,11 @@ public class NotificationsFragment extends SFragment implements
}
private void sendFetchNotificationsRequest(final String fromId, String uptoId) {
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
}
listCall = api.notifications(fromId, uptoId, null);
listCall = mastodonAPI.notifications(fromId, uptoId, null);
listCall.enqueue(new Callback<List<Notification>>() {
@Override
@ -168,6 +196,10 @@ public class NotificationsFragment extends SFragment implements
sendFetchNotificationsRequest(null, null);
}
public void removePostsByUser(String accountId) {
adapter.removeAllByAccountId(accountId);
}
private static boolean findNotification(List<Notification> notifications, String id) {
for (Notification notification : notifications) {
if (notification.id.equals(id)) {
@ -192,6 +224,11 @@ public class NotificationsFragment extends SFragment implements
} else {
adapter.update(notifications);
}
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
} else if (fromId != null) {
adapter.setFooterState(NotificationsAdapter.FooterState.END);
}
swipeRefreshLayout.setRefreshing(false);
}
@ -245,4 +282,11 @@ public class NotificationsFragment extends SFragment implements
public void onViewAccount(String id) {
super.viewAccount(id);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if(key.equals("fabHide")) {
hideFab = sharedPreferences.getBoolean("fabHide", false);
}
}
}

@ -37,9 +37,12 @@ import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.ConnectionSpec;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
class OkHttpUtils {
public class OkHttpUtils {
static final String TAG = "OkHttpUtils"; // logging tag
/**
@ -55,8 +58,7 @@ class OkHttpUtils {
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20.
*/
@NonNull
static OkHttpClient.Builder getCompatibleClientBuilder() {
public static OkHttpClient.Builder getCompatibleClientBuilder() {
ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites()
.supportsTlsExtensions(true)
@ -69,16 +71,37 @@ class OkHttpUtils {
specList.add(ConnectionSpec.CLEARTEXT);
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.addInterceptor(getUserAgentInterceptor())
.connectionSpecs(specList);
return enableHigherTlsOnPreLollipop(builder);
}
@NonNull
static OkHttpClient getCompatibleClient() {
public static OkHttpClient getCompatibleClient() {
return getCompatibleClientBuilder().build();
}
/**
* Add a custom User-Agent that contains Tusky & Android Version to all requests
* Example:
* User-Agent: Tusky/1.1.2 Android/5.0.2
*/
@NonNull
private static Interceptor getUserAgentInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request requestWithUserAgent = originalRequest.newBuilder()
.header("User-Agent", "Tusky/"+BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE)
.build();
return chain.proceed(requestWithUserAgent);
}
};
}
/**
* Android version Nougat has a regression where elliptic curve cipher suites are supported, but
* only the curve secp256r1 is allowed. So, first it's best to just disable all elliptic
@ -194,7 +217,7 @@ class OkHttpUtils {
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
int localPort) throws IOException {
int localPort) throws IOException {
return patch(delegate.createSocket(address, port, localAddress, localPort));
}

@ -15,7 +15,6 @@
package com.keylesspalace.tusky;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -37,6 +36,7 @@ import java.util.List;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/* 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
@ -44,22 +44,32 @@ import retrofit2.Callback;
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
* up what needs to be where. */
public class SFragment extends BaseFragment {
public abstract class SFragment extends BaseFragment {
interface OnUserRemovedListener {
void onUserRemoved(String accountId);
}
protected String loggedInAccountId;
protected String loggedInUsername;
protected MastodonAPI mastodonAPI;
protected OnUserRemovedListener userRemovedListener;
protected static int COMPOSE_RESULT = 1;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences preferences = getContext().getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
SharedPreferences preferences = getPrivatePreferences();
loggedInAccountId = preferences.getString("loggedInAccountId", null);
loggedInUsername = preferences.getString("loggedInAccountUsername", null);
}
public MastodonAPI getApi() {
return ((BaseActivity) getActivity()).mastodonAPI;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
BaseActivity activity = (BaseActivity) getActivity();
mastodonAPI = activity.mastodonAPI;
userRemovedListener = (OnUserRemovedListener) activity;
}
protected void reply(Status status) {
@ -79,11 +89,22 @@ public class SFragment extends BaseFragment {
intent.putExtra("reply_visibility", replyVisibility);
intent.putExtra("content_warning", contentWarning);
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
startActivity(intent);
startActivityForResult(intent, COMPOSE_RESULT);
}
public void onSuccessfulStatus() {}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
onSuccessfulStatus();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
protected void reblog(final Status status, final boolean reblog,
final RecyclerView.Adapter adapter, final int position) {
final RecyclerView.Adapter adapter, final int position) {
String id = status.getActionableId();
Callback<Status> cb = new Callback<Status>() {
@ -101,16 +122,14 @@ public class SFragment extends BaseFragment {
}
@Override
public void onFailure(Call<Status> call, Throwable t) {
}
public void onFailure(Call<Status> call, Throwable t) {}
};
Call<Status> call;
if (reblog) {
call = getApi().reblogStatus(id);
call = mastodonAPI.reblogStatus(id);
} else {
call = getApi().unreblogStatus(id);
call = mastodonAPI.unreblogStatus(id);
}
call.enqueue(cb);
callList.add(call);
@ -135,55 +154,59 @@ public class SFragment extends BaseFragment {
}
@Override
public void onFailure(Call<Status> call, Throwable t) {
}
public void onFailure(Call<Status> call, Throwable t) {}
};
Call<Status> call;
if (favourite) {
call = getApi().favouriteStatus(id);
call = mastodonAPI.favouriteStatus(id);
} else {
call = getApi().unfavouriteStatus(id);
call = mastodonAPI.unfavouriteStatus(id);
}
call.enqueue(cb);
callList.add(call);
}
private void block(String id) {
Call<Relationship> call = getApi().blockAccount(id);
private void mute(String id) {
Call<Relationship> call = mastodonAPI.muteAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {}
});
callList.add(call);
userRemovedListener.onUserRemoved(id);
}
private void block(String id) {
Call<Relationship> call = mastodonAPI.blockAccount(id);
call.enqueue(new Callback<Relationship>() {
@Override
public void onFailure(Call<Relationship> call, Throwable t) {
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
}
@Override
public void onFailure(Call<Relationship> call, Throwable t) {}
});
callList.add(call);
userRemovedListener.onUserRemoved(id);
}
private void delete(String id) {
Call<ResponseBody> call = getApi().deleteStatus(id);
Call<ResponseBody> call = mastodonAPI.deleteStatus(id);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
}
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
public void onFailure(Call<ResponseBody> call, Throwable t) {}
});
callList.add(call);
}
protected void more(Status status, View view, final AdapterItemRemover adapter,
final int position) {
protected void more(final Status status, View view, final AdapterItemRemover adapter,
final int position) {
final String id = status.getActionableId();
final String accountId = status.getActionableStatus().account.id;
final String accountUsename = status.getActionableStatus().account.username;
@ -201,12 +224,29 @@ public class SFragment extends BaseFragment {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.status_share: {
case R.id.status_share_content: {
StringBuilder sb = new StringBuilder();
sb.append(status.account.username);
sb.append(" - ");
sb.append(status.content.toString());
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true;
}
case R.id.status_share_link: {
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)));
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
return true;
}
case R.id.status_mute: {
mute(accountId);
return true;
}
case R.id.status_block: {

@ -25,7 +25,7 @@ import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
public 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));

@ -19,13 +19,11 @@ import android.view.View;
import com.keylesspalace.tusky.entity.Status;
interface StatusActionListener {
interface StatusActionListener extends LinkListener {
void onReply(int position);
void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, final int position);
void onMore(View view, final int position);
void onViewMedia(String url, Status.MediaAttachment.Type type);
void onViewThread(int position);
void onViewTag(String tag);
void onViewAccount(String id);
}

@ -0,0 +1,20 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
interface StatusRemoveListener {
void removePostsByUser(String accountId);
}

@ -16,14 +16,9 @@
package com.keylesspalace.tusky;
import android.content.Context;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageButton;
@ -102,57 +97,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
}
private void setContent(Spanned content, Status.Mention[] mentions,
final StatusActionListener listener) {
StatusActionListener listener) {
/* Redirect URLSpan's in the status content to the listener for viewing tag pages and
* account pages. */
SpannableStringBuilder builder = new SpannableStringBuilder(content);
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(container.getContext()).getBoolean("customTabs", true);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence text = builder.subSequence(start, end);
if (text.charAt(0) == '#') {
final String tag = text.subSequence(1, text.length()).toString();
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewTag(tag);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
} else if (text.charAt(0) == '@') {
final String accountUsername = text.subSequence(1, text.length()).toString();
String id = null;
for (Status.Mention mention: mentions) {
if (mention.username.equals(accountUsername)) {
id = mention.id;
}
}
if (id != null) {
final String accountId = id;
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewAccount(accountId);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
} else if (useCustomTabs) {
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
}
// Set the contents.
this.content.setText(builder);
// Make links clickable.
this.content.setLinksClickable(true);
this.content.setMovementMethod(LinkMovementMethod.getInstance());
LinkHelper.setClickableText(this.content, content, mentions, listener);
}
private void setAvatar(String url) {
@ -230,7 +178,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
}
private void setMediaPreviews(final Status.MediaAttachment[] attachments,
boolean sensitive, final StatusActionListener listener) {
boolean sensitive, final StatusActionListener listener) {
final ImageView[] previews = {
mediaPreview0,
mediaPreview1,
@ -249,20 +197,32 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
previews[i].setVisibility(View.VISIBLE);
Picasso.with(context)
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.into(previews[i]);
if(previewUrl == null || previewUrl.isEmpty()) {
Picasso.with(context)
.load(mediaPreviewUnloadedId)
.into(previews[i]);
} else {
Picasso.with(context)
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.into(previews[i]);
}
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) {
listener.onViewMedia(url, type);
}
});
if(url == null || url.isEmpty()) {
previews[i].setOnClickListener(null);
} else {
previews[i].setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewMedia(url, type);
}
});
}
}
if (sensitive) {

@ -0,0 +1,31 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
/**
* This is just a wrapper class for a String.
*
* It was designed to get around the limitation of a Json deserializer which only allows custom
* deserializing based on types, when special handling for a specific field was what was actually
* desired (in this case, display names). So, it was most expedient to just make up a type.
*/
public class StringWithEmoji {
public String value;
public StringWithEmoji(String value) {
this.value = value;
}
}

@ -0,0 +1,38 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import 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;
/** This is a type-based workaround to allow for shortcode conversion when loading display names. */
class StringWithEmojiTypeAdapter implements JsonDeserializer<StringWithEmoji> {
@Override
public StringWithEmoji deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
String value = json.getAsString();
if (value != null) {
return new StringWithEmoji(Emojione.shortnameToUnicode(value, false));
} else {
return new StringWithEmoji("");
}
}
}

@ -65,20 +65,55 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
notifyItemRemoved(position);
}
int insertStatus(Status status) {
public void removeAllByAccountId(String accountId) {
for (int i = 0; i < statuses.size();) {
Status status = statuses.get(i);
if (accountId.equals(status.account.id)) {
statuses.remove(i);
notifyItemRemoved(i);
} else {
i += 1;
}
}
}
int setStatus(Status status) {
if (statuses.size() > 0 && statuses.get(statusIndex).equals(status)) {
// Do not add this status on refresh, it's already in there.
statuses.set(statusIndex, status);
return statusIndex;
}
int i = statusIndex;
statuses.add(i, status);
notifyItemInserted(i);
return i;
}
void addAncestors(List<Status> ancestors) {
void setContext(List<Status> ancestors, List<Status> descendants) {
Status mainStatus = null;
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
// as we have no guarantee on their order to be the same as before
int old_size = statuses.size();
if (old_size > 0) {
mainStatus = statuses.get(statusIndex);
statuses.clear();
notifyItemRangeRemoved(0, old_size);
}
// Insert newly fetched ancestors
statusIndex = ancestors.size();
statuses.addAll(0, ancestors);
notifyItemRangeInserted(0, statusIndex);
}
void addDescendants(List<Status> descendants) {
if (mainStatus != null) {
// In case we needed to delete everything (which is way easier than deleting
// everything except one), re-insert the remaining status here.
statuses.add(statusIndex, mainStatus);
notifyItemInserted(statusIndex);
}
// Insert newly fetched descendants
int end = statuses.size();
statuses.addAll(descendants);
notifyItemRangeInserted(end, descendants.size());

@ -30,8 +30,15 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1;
enum FooterState {
EMPTY,
END,
LOADING
}
private List<Status> statuses;
private StatusActionListener statusListener;
private FooterState footerState = FooterState.END;
TimelineAdapter(StatusActionListener statusListener) {
super();
@ -49,13 +56,37 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
break;
case END: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_end, viewGroup, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_empty, viewGroup, false);
break;
}
}
return new FooterViewHolder(view);
}
}
}
void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(statuses.size());
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < statuses.size()) {
@ -111,6 +142,18 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
notifyItemRemoved(position);
}
void removeAllByAccountId(String accountId) {
for (int i = 0; i < statuses.size();) {
Status status = statuses.get(i);
if (accountId.equals(status.account.id)) {
statuses.remove(i);
notifyItemRemoved(i);
} else {
i += 1;
}
}
}
@Nullable
Status getItem(int position) {
if (position >= 0 && position < statuses.size()) {

@ -39,7 +39,10 @@ import retrofit2.Call;
import retrofit2.Callback;
public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
StatusRemoveListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "Timeline"; // logging tag
private Call<List<Status>> listCall;
@ -61,6 +64,7 @@ public class TimelineFragment extends SFragment implements
private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
private boolean hideFab;
public static TimelineFragment newInstance(Kind kind) {
TimelineFragment fragment = new TimelineFragment();
@ -145,24 +149,28 @@ public class TimelineFragment extends SFragment implements
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then. */
if (followButtonPresent()) {
if (composeButtonPresent()) {
/* Use a modified scroll listener that both loads more statuses as it goes, and hides
* the follow button on down-scroll. */
MainActivity activity = (MainActivity) getActivity();
final FloatingActionButton composeButton = activity.composeButton;
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
activity);
preferences.registerOnSharedPreferenceChangeListener(this);
hideFab = preferences.getBoolean("fabHide", false);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
super.onScrolled(view, dx, dy);
if (preferences.getBoolean("fabHide", false)) {
if (hideFab) {
if (dy > 0 && composeButton.isShown()) {
composeButton.hide(); // hides the button if we're scrolling down
} else if (dy < 0 && !composeButton.isShown()) {
composeButton.show(); // shows it if we are scrolling up
}
} else if (!composeButton.isShown()) {
composeButton.show();
}
}
@ -202,7 +210,7 @@ public class TimelineFragment extends SFragment implements
return kind != Kind.TAG && kind != Kind.FAVOURITES;
}
private boolean followButtonPresent() {
private boolean composeButtonPresent() {
return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.USER;
}
@ -212,7 +220,9 @@ public class TimelineFragment extends SFragment implements
}
private void sendFetchTimelineRequest(@Nullable final String fromId, @Nullable String uptoId) {
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
}
Callback<List<Status>> cb = new Callback<List<Status>>() {
@Override
@ -233,27 +243,27 @@ public class TimelineFragment extends SFragment implements
switch (kind) {
default:
case HOME: {
listCall = api.homeTimeline(fromId, uptoId, null);
listCall = mastodonAPI.homeTimeline(fromId, uptoId, null);
break;
}
case PUBLIC_FEDERATED: {
listCall = api.publicTimeline(null, fromId, uptoId, null);
listCall = mastodonAPI.publicTimeline(null, fromId, uptoId, null);
break;
}
case PUBLIC_LOCAL: {
listCall = api.publicTimeline(true, fromId, uptoId, null);
listCall = mastodonAPI.publicTimeline(true, fromId, uptoId, null);
break;
}
case TAG: {
listCall = api.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
listCall = mastodonAPI.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
break;
}
case USER: {
listCall = api.accountStatuses(hashtagOrId, fromId, uptoId, null);
listCall = mastodonAPI.accountStatuses(hashtagOrId, fromId, uptoId, null);
break;
}
case FAVOURITES: {
listCall = api.favourites(fromId, uptoId, null);
listCall = mastodonAPI.favourites(fromId, uptoId, null);
break;
}
}
@ -265,6 +275,10 @@ public class TimelineFragment extends SFragment implements
sendFetchTimelineRequest(null, null);
}
public void removePostsByUser(String accountId) {
adapter.removeAllByAccountId(accountId);
}
private static boolean findStatus(List<Status> statuses, String id) {
for (Status status : statuses) {
if (status.id.equals(id)) {
@ -282,6 +296,11 @@ public class TimelineFragment extends SFragment implements
} else {
adapter.update(statuses);
}
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
} else if(fromId != null) {
adapter.setFooterState(TimelineAdapter.FooterState.END);
}
swipeRefreshLayout.setRefreshing(false);
}
@ -299,6 +318,14 @@ public class TimelineFragment extends SFragment implements
}
}
@Override
public void onSuccessfulStatus() {
if (kind == Kind.HOME || kind == Kind.PUBLIC_FEDERATED || kind == Kind.PUBLIC_LOCAL) {
onRefresh();
}
super.onSuccessfulStatus();
}
public void onReply(int position) {
super.reply(adapter.getItem(position));
}
@ -339,4 +366,11 @@ public class TimelineFragment extends SFragment implements
}
super.viewAccount(id);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if(key.equals("fabHide")) {
hideFab = sharedPreferences.getBoolean("fabHide", false);
}
}
}

@ -18,10 +18,35 @@ package com.keylesspalace.tusky;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
class TimelinePagerAdapter extends FragmentPagerAdapter {
private int currentFragmentIndex;
private List<Fragment> registeredFragments;
TimelinePagerAdapter(FragmentManager manager) {
super(manager);
currentFragmentIndex = 0;
registeredFragments = new ArrayList<>();
}
Fragment getCurrentFragment() {
return registeredFragments.get(currentFragmentIndex);
}
List<Fragment> getRegisteredFragments() {
return registeredFragments;
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
if (position != currentFragmentIndex) {
currentFragmentIndex = position;
}
super.setPrimaryItem(container, position, object);
}
@Override
@ -54,4 +79,17 @@ class TimelinePagerAdapter extends FragmentPagerAdapter {
public CharSequence getPageTitle(int position) {
return null;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.add(fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove((Fragment) object);
super.destroyItem(container, position, object);
}
}

@ -0,0 +1,49 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.Application;
import android.net.Uri;
import com.squareup.picasso.Picasso;
import com.jakewharton.picasso.OkHttp3Downloader;
public class TuskyApplication extends Application {
@Override
public void onCreate() {
// Initialize Picasso configuration
Picasso.Builder builder = new Picasso.Builder(this);
builder.downloader(new OkHttp3Downloader(this));
if (BuildConfig.DEBUG) {
builder.listener(new Picasso.Listener() {
@Override
public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
exception.printStackTrace();
}
});
}
try {
Picasso.setSingletonInstance(builder.build());
} catch (IllegalStateException e) {
throw new RuntimeException(e);
}
if (BuildConfig.DEBUG) {
Picasso.with(this).setLoggingEnabled(true);
}
}
}

@ -15,9 +15,21 @@
package com.keylesspalace.tusky;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.app.DialogFragment;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@ -27,6 +39,8 @@ import android.view.WindowManager;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import java.io.File;
import butterknife.BindView;
import butterknife.ButterKnife;
import uk.co.senab.photoview.PhotoView;
@ -35,6 +49,9 @@ import uk.co.senab.photoview.PhotoViewAttacher;
public class ViewMediaFragment extends DialogFragment {
private PhotoViewAttacher attacher;
private DownloadManager downloadManager;
private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
@BindView(R.id.view_media_image) PhotoView photoView;
@ -99,6 +116,25 @@ public class ViewMediaFragment extends DialogFragment {
}
});
attacher.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
AlertDialog downloadDialog = new AlertDialog.Builder(getContext()).create();
downloadDialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.dialog_download_image),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
downloadImage();
}
});
downloadDialog.show();
return false;
}
});
Picasso.with(getContext())
.load(url)
.into(photoView, new Callback() {
@ -121,4 +157,63 @@ public class ViewMediaFragment extends DialogFragment {
attacher.cleanup();
super.onDestroyView();
}
private void downloadImage(){
//Permission stuff
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
ContextCompat.checkSelfPermission(this.getContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
android.support.v4.app.ActivityCompat.requestPermissions(getActivity(),
new String[] { android.Manifest.permission.WRITE_EXTERNAL_STORAGE },
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
} else {
//download stuff
String url = getArguments().getString("url");
Uri uri = Uri.parse(url);
String filename = new File(url).getName();
downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.allowScanningByMediaScanner();
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, getString(R.string.app_name) + "/" + filename);
downloadManager.enqueue(request);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadImage();
} else {
doErrorDialog(R.string.error_media_download_permission, R.string.action_retry,
new View.OnClickListener() {
@Override
public void onClick(View v) {
downloadImage();
}
});
}
break;
}
}
}
private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId,
View.OnClickListener listener) {
Snackbar bar = Snackbar.make(getView(), getString(descriptionId),
Snackbar.LENGTH_SHORT);
bar.setAction(actionId, listener);
bar.show();
}
}

@ -26,7 +26,9 @@ import android.view.MenuItem;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ViewTagActivity extends BaseActivity {
public class ViewTagActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
private Fragment timelineFragment;
@BindView(R.id.toolbar) Toolbar toolbar;
@Override
@ -51,6 +53,8 @@ public class ViewTagActivity extends BaseActivity {
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag);
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
timelineFragment = fragment;
}
@Override
@ -63,4 +67,10 @@ public class ViewTagActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public void onUserRemoved(String accountId) {
StatusRemoveListener listener = (StatusRemoveListener) timelineFragment;
listener.removePostsByUser(accountId);
}
}

@ -24,7 +24,9 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
public class ViewThreadActivity extends BaseActivity {
public class ViewThreadActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
Fragment viewThreadFragment;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -44,6 +46,8 @@ public class ViewThreadActivity extends BaseActivity {
Fragment fragment = ViewThreadFragment.newInstance(id);
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
viewThreadFragment = fragment;
}
@Override
@ -62,4 +66,12 @@ public class ViewThreadActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
public void onUserRemoved(String accountId) {
if (viewThreadFragment instanceof StatusRemoveListener) {
StatusRemoveListener listener = (StatusRemoveListener) viewThreadFragment;
listener.removePostsByUser(accountId);
}
}
}

@ -21,6 +21,7 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -34,9 +35,11 @@ import com.keylesspalace.tusky.entity.StatusContext;
import retrofit2.Call;
import retrofit2.Callback;
public class ViewThreadFragment extends SFragment implements StatusActionListener {
public class ViewThreadFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener {
private static final String TAG = "ViewThreadFragment";
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private ThreadAdapter adapter;
private String thisThreadsStatusId;
@ -56,6 +59,9 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false);
Context context = getContext();
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
swipeRefreshLayout.setOnRefreshListener(this);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
@ -86,7 +92,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
if (response.isSuccessful()) {
int position = adapter.insertStatus(response.body());
int position = adapter.setStatus(response.body());
recyclerView.scrollToPosition(position);
} else {
onThreadRequestFailure(id);
@ -109,10 +115,10 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
@Override
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
if (response.isSuccessful()) {
swipeRefreshLayout.setRefreshing(false);
StatusContext context = response.body();
adapter.addAncestors(context.ancestors);
adapter.addDescendants(context.descendants);
adapter.setContext(context.ancestors, context.descendants);
} else {
onThreadRequestFailure(id);
}
@ -128,6 +134,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
private void onThreadRequestFailure(final String id) {
View view = getView();
swipeRefreshLayout.setRefreshing(false);
if (view != null) {
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry, new View.OnClickListener() {
@ -143,6 +150,22 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
}
}
@Override
public void removePostsByUser(String accountId) {
adapter.removeAllByAccountId(accountId);
}
public void onRefresh() {
sendStatusRequest(thisThreadsStatusId);
sendThreadRequest(thisThreadsStatusId);
}
@Override
public void onSuccessfulStatus() {
onRefresh();
super.onSuccessfulStatus();
}
public void onReply(int position) {
super.reply(adapter.getItem(position));
}

@ -21,6 +21,7 @@ import android.text.Spanned;
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion;
import com.google.gson.annotations.SerializedName;
import com.keylesspalace.tusky.HtmlUtils;
import com.keylesspalace.tusky.StringWithEmoji;
public class Account implements SearchSuggestion {
public String id;
@ -32,7 +33,7 @@ public class Account implements SearchSuggestion {
public String username;
@SerializedName("display_name")
public String displayName;
public StringWithEmoji displayName;
public Spanned note;
@ -70,11 +71,10 @@ public class Account implements SearchSuggestion {
}
public String getDisplayName() {
if (displayName.length() == 0) {
if (displayName.value.length() == 0) {
return localUsername;
}
return displayName;
return displayName.value;
}
@Override
@ -92,7 +92,7 @@ public class Account implements SearchSuggestion {
dest.writeString(id);
dest.writeString(localUsername);
dest.writeString(username);
dest.writeString(displayName);
dest.writeString(displayName.value);
dest.writeString(HtmlUtils.toHtml(note));
dest.writeString(url);
dest.writeString(avatar);
@ -111,7 +111,7 @@ public class Account implements SearchSuggestion {
id = in.readString();
localUsername = in.readString();
username = in.readString();
displayName = in.readString();
displayName = new StringWithEmoji(in.readString());
note = HtmlUtils.fromHtml(in.readString());
url = in.readString();
avatar = in.readString();

@ -0,0 +1,19 @@
package com.keylesspalace.tusky.entity;
import com.google.gson.annotations.SerializedName;
public class Profile {
@SerializedName("display_name")
public String displayName;
@SerializedName("note")
public String note;
/** Encoded in Base-64 */
@SerializedName("avatar")
public String avatar;
/** Encoded in Base-64 */
@SerializedName("header")
public String header;
}

@ -146,5 +146,8 @@ public class Status {
@SerializedName("acct")
public String username;
@SerializedName("username")
public String localUsername;
}
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/button_dark" android:state_checked="true" />
<item android:color="?material_drawer_primary_icon" />
</selector>

@ -0,0 +1,12 @@
<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,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>

@ -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="#FFFFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="35.43307"
android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M32.16,4.46L31.62,5.01L14.63,21.99L5.78,13.13L2.5,16.41L14.52,28.43L14.55,28.41L14.66,28.52L35.44,7.74L34.89,7.19C34.17,6.46 33.44,5.74 32.71,5.01L32.16,4.46z" />
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m1.1,6.19c-0.58,0 -1.07,0.49 -1.07,1.07l0,23.06c0,0.58 0.49,1.07 1.07,1.07l23.06,0c0.58,0 1.07,-0.49 1.07,-1.07l0,-18.89 -1.54,1.54 0,16.88 -22.12,0 0,-22.12 22.12,0 0,2.83 1.54,-1.54 0,-1.76c0,-0.58 -0.49,-1.07 -1.07,-1.07l-23.06,0z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" />
</vector>

@ -1,27 +1,7 @@
<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="M31.89,5.82m-5.31,0a5.31,5.31 0,1 1,10.63 0a5.31,5.31 0,1 1,-10.63 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M10.63,5.82m-5.31,0a5.31,5.31 0,1 1,10.63 0a5.31,5.31 0,1 1,-10.63 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M21.26,23.03m-5.31,0a5.31,5.31 0,1 1,10.63 0a5.31,5.31 0,1 1,-10.63 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m17.62,29.22c-2.07,1.24 -3.45,3.49 -3.45,6.08l0,3.54c0,3.93 -0.38,3.54 3.54,3.54l7.09,0c3.93,0 3.54,0.38 3.54,-3.54l0,-3.54c0,-2.59 -1.38,-4.84 -3.45,-6.08a7.19,7.19 0,0 1,-3.64 1,7.19 7.19,0 0,1 -3.64,-1z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m28.25,12.04c-1.69,1.01 -2.91,2.72 -3.3,4.72a7.28,7.28 0,0 1,3.59 6.27,7.28 7.28,0 0,1 -0.33,2.18c0.06,-0 0.08,0 0.14,0l7.09,0c3.93,0 3.54,0.38 3.54,-3.54l0,-3.54c0,-2.59 -1.38,-4.84 -3.45,-6.08a7.19,7.19 0,0 1,-3.64 1,7.19 7.19,0 0,1 -3.64,-1z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m6.99,12.04c-2.07,1.24 -3.45,3.49 -3.45,6.08l0,3.54c0,3.93 -0.38,3.54 3.54,3.54l7.09,0c0.07,0 0.08,-0 0.15,0a7.28,7.28 0,0 1,-0.34 -2.18,7.28 7.28,0 0,1 3.59,-6.27c-0.39,-2.01 -1.61,-3.71 -3.3,-4.72a7.19,7.19 0,0 1,-3.64 1,7.19 7.19,0 0,1 -3.64,-1z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="0.30000001"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,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,-2zM18,20L6,20L6,10h12v10z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,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,-2zM8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2L8.9,8L8.9,6zM18,20L6,20L6,10h12v10z" />
</vector>

@ -0,0 +1,11 @@
<vector android:height="24dp" android:viewportHeight="35.43307"
android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m17.72,3.54 l-8.86,8.86 -7.09,0 0,10.63 7.09,0 8.86,8.86z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m22.86,11.45 l-2.51,2.51 3.76,3.76 -3.76,3.76 2.51,2.51 3.76,-3.76 3.76,3.76 2.5,-2.51 -3.76,-3.76 3.76,-3.76 -2.5,-2.51 -3.76,3.76 -3.76,-3.76z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/>
</vector>

@ -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="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

@ -0,0 +1,19 @@
<vector android:height="24dp" android:viewportHeight="35.43307"
android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m17.72,3.54 l-8.86,8.86 -7.09,0 0,10.63 7.09,0 8.86,8.86z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m21.47,13.96a5.31,5.31 0,0 1,1.56 3.76,5.31 5.31,0 0,1 -1.56,3.76"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m28.99,6.44a15.94,15.94 0,0 1,4.67 11.27,15.94 15.94,0 0,1 -4.67,11.27"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/>
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="m25.23,10.2a10.63,10.63 0,0 1,3.11 7.52,10.63 10.63,0 0,1 -3.11,7.52"
android:strokeAlpha="1" android:strokeColor="#ffffff"
android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@ -5,7 +5,7 @@
android:id="@+id/activity_view_thread"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.BlocksActivity">
tools:context="com.keylesspalace.tusky.AccountListActivity">
<LinearLayout
android:layout_width="match_parent"

@ -19,6 +19,33 @@
android:background="@android:color/transparent"
android:elevation="4dp" />
<LinearLayout
android:id="@+id/compose_content_warning_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<EditText
android:id="@+id/field_content_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:ems="10"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:hint="@string/hint_content_warning"
android:inputType="text|textCapSentences" />
<View
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/compose_edit_area"
android:layout_width="match_parent"
@ -56,32 +83,6 @@
</RelativeLayout>
<LinearLayout
android:id="@+id/compose_content_warning_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<EditText
android:id="@+id/field_content_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:ems="10"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:hint="@string/hint_content_warning"
android:inputType="text|textCapSentences" />
<View
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -91,6 +92,15 @@
android:paddingRight="16dp"
android:paddingTop="4dp">
<ImageButton
android:id="@+id/compose_photo_take"
style="?attr/image_button_style"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
app:srcCompat="@drawable/ic_camera_24dp"
android:contentDescription="@string/action_photo_take" />
<ImageButton
android:id="@+id/compose_photo_pick"
style="?attr/image_button_style"

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.EditProfileActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
android:elevation="4dp" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_display_name"
android:hint="@string/hint_display_name"
android:maxLength="30"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_note"
android:hint="@string/hint_note"
android:maxLength="160"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_avatar"
android:labelFor="@+id/edit_profile_avatar"
android:layout_marginRight="8dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@id/edit_profile_avatar"
android:text="@string/action_photo_pick"
android:textColor="@color/text_color_primary_dark" />
</LinearLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:id="@+id/edit_profile_avatar_preview"
android:contentDescription="@null"
android:visibility="gone" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_avatar_progress"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_header"
android:labelFor="@+id/edit_profile_header"
android:layout_marginRight="8dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@id/edit_profile_header"
android:text="@string/action_photo_pick"
android:textColor="@color/text_color_primary_dark" />
</LinearLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp">
<ImageView
android:layout_width="167.2dp"
android:layout_height="80dp"
android:id="@+id/edit_profile_header_preview"
android:contentDescription="@null"
android:visibility="gone" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_profile_header_progress"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/edit_profile_save_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
android:layout_gravity="center" />
</android.support.design.widget.CoordinatorLayout>

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -28,10 +27,12 @@
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:paddingTop="?attr/actionBarSize"
app:tabTextAppearance="@style/TabLayoutTextStyle"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabPaddingEnd="1dp"
app:tabPaddingStart="1dp"
app:tabPaddingTop="4dp"
app:tabPaddingEnd="1dp">
app:tabTextAppearance="@style/TabLayoutTextStyle">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
@ -63,34 +64,34 @@
android:id="@+id/floating_search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:floatingSearch_close_search_on_keyboard_dismiss="true"
app:floatingSearch_leftActionMode="showHamburger"
app:floatingSearch_searchBarMarginLeft="6dp"
app:floatingSearch_searchBarMarginTop="4dp"
app:floatingSearch_searchBarMarginRight="6dp"
app:floatingSearch_searchBarMarginTop="4dp"
app:floatingSearch_searchHint="@string/search"
app:floatingSearch_suggestionsListAnimDuration="250"
app:floatingSearch_showSearchKey="false"
app:floatingSearch_leftActionMode="showHamburger"
app:floatingSearch_close_search_on_keyboard_dismiss="true"/>
app:floatingSearch_suggestionsListAnimDuration="250" />
<View
android:id="@+id/tab_bottom_shadow"
android:layout_width="match_parent"
android:layout_height="2dp"
app:layout_anchor="@id/tab_layout"
app:layout_anchorGravity="bottom"
android:background="@drawable/material_drawer_shadow_bottom"
android:visibility="visible" />
android:visibility="visible"
app:layout_anchor="@id/tab_layout"
app:layout_anchorGravity="bottom" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/floating_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:clickable="true"
android:contentDescription="@string/action_compose"
app:layout_anchor="@id/pager"
app:layout_anchorGravity="bottom|end"
android:clickable="true"
android:layout_margin="16dp"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_create_24dp"
android:contentDescription="@string/action_compose" />
app:srcCompat="@drawable/ic_create_24dp" />
<FrameLayout
android:id="@+id/overlay_fragment_container"

@ -15,23 +15,48 @@
<RadioButton
android:text="@string/visibility_public"
android:button="@drawable/ic_public_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_public"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_unlisted"
android:button="@drawable/ic_lock_open_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_unlisted"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_private"
android:button="@drawable/ic_lock_outline_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_private"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_direct"
android:button="@drawable/ic_email_24dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_direct"
android:layout_marginTop="5dp"
android:paddingLeft="10dp"
android:buttonTint="@color/drawer_visibility_panel_item"
android:layout_weight="1" />
</RadioGroup>

@ -1,7 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>

@ -13,7 +13,8 @@
android:id="@+id/blocked_user_avatar"
android:layout_alignParentLeft="true"
android:layout_marginRight="24dp"
android:layout_centerVertical="true"/>
android:layout_centerVertical="true"
android:contentDescription="@string/action_view_profile" />
<ImageButton
app:srcCompat="@drawable/ic_clear_24dp"

@ -0,0 +1,80 @@
<?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="72dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical">
<com.pkmmte.view.CircularImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@+id/follow_request_avatar"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginRight="24dp"
android:layout_marginEnd="24dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_view_profile" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_toRightOf="@id/follow_request_avatar"
android:layout_toEndOf="@id/follow_request_avatar"
android:layout_toLeftOf="@+id/follow_request_accept"
android:layout_toStartOf="@id/follow_request_accept">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/follow_request_display_name"
android:text="Display name"
android:maxLines="1"
android:ellipsize="end"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:textStyle="normal|bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\@username"
android:maxLines="1"
android:ellipsize="end"
android:textSize="14sp"
android:id="@+id/follow_request_username"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
style="?attr/image_button_style"
android:id="@+id/follow_request_accept"
app:srcCompat="@drawable/ic_check_24dp"
android:layout_toLeftOf="@+id/follow_request_reject"
android:layout_toStartOf="@id/follow_request_reject"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_accept" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
style="?attr/image_button_style"
android:id="@id/follow_request_reject"
app:srcCompat="@drawable/ic_reject_24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_reject" />
</RelativeLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/elephant_friend" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/footer_empty"
android:textAlignment="center" />
</LinearLayout>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
</LinearLayout>

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="72dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical">
<com.pkmmte.view.CircularImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@+id/muted_user_avatar"
android:layout_alignParentLeft="true"
android:layout_marginRight="24dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_view_profile" />
<ImageButton
app:srcCompat="@drawable/ic_unmute_24dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/muted_user_unmute"
android:layout_gravity="center_vertical"
style="?attr/image_button_style"
android:layout_alignParentRight="true"
android:layout_marginLeft="16dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_unmute" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_toRightOf="@id/muted_user_avatar"
android:layout_toLeftOf="@id/muted_user_unmute">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/muted_user_display_name"
android:text="Display name"
android:maxLines="1"
android:ellipsize="end"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:textStyle="normal|bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\@username"
android:maxLines="1"
android:ellipsize="end"
android:textSize="14sp"
android:id="@+id/muted_user_username"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
</RelativeLayout>

@ -9,6 +9,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title"
android:layout_marginTop="8dp"
android:layout_centerHorizontal="true"
android:textAllCaps="true"
android:textStyle="normal|bold" />

@ -0,0 +1,9 @@
<?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_save"
android:icon="@drawable/ic_check_in_box_24dp"
android:title="@string/action_save"
app:showAsAction="always" />
</menu>

@ -2,7 +2,18 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/status_share"
android:title="@string/action_share"/>
android:title="@string/action_share">
<menu>
<item
android:id="@+id/status_share_link"
android:title="@string/status_share_link" />
<item
android:id="@+id/status_share_content"
android:title="@string/status_share_content"/>
</menu>
</item>
<item android:title="@string/action_mute"
android:id="@+id/status_mute" />
<item android:title="@string/action_block"
android:id="@+id/status_block" />
<item android:title="@string/action_report"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@ -0,0 +1,160 @@
<resources>
<string name="error_generic">وقع هناك خطأ.</string>
<!-- <string name="error_empty">This cannot be empty.</string> -->
<string name="error_invalid_domain">اسم النطاق غير صالح</string>
<string name="error_failed_app_registration">اخفقت المصادقة مع مثيل الخادم هذا.</string>
<string name="error_no_web_browser_found">لم يتم العثور على متصفح قابل للإستعمال.</string>
<!-- <string name="error_authorization_unknown">An unidentified authorization error occurred.</string> -->
<string name="error_authorization_denied">تم رفض التصريح.</string>
<!-- <string name="error_retrieving_oauth_token">Failed getting a login token.</string> -->
<!-- <string name="error_compose_character_limit">The status is too long!</string> -->
<string name="error_media_upload_size">يجب أن يكون حجم الملف أقل من 4 ميغابايت.</string>
<string name="error_media_upload_type">لا يمكن رفع هذا النوع من الملفات.</string>
<string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string>
<!-- <string name="error_media_upload_permission">Permission to read media is required.</string> -->
<!-- <string name="error_media_download_permission">Permission to store media is required.</string> -->
<!-- <string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same status.</string> -->
<string name="error_media_upload_sending">اخفقت عملية الرفع.</string>
<!-- <string name="error_report_too_few_statuses">At least one status must be reported.</string> -->
<string name="title_home">الرئيسية</string>
<string name="title_notifications">الاشعارات</string>
<string name="title_public_local">المحلية</string>
<string name="title_public_federated">الفدرالية</string>
<string name="title_thread">الخيط</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">المشاركات</string>
<string name="title_follows">يتبع</string>
<string name="title_followers">المتابعون</string>
<string name="title_favourites">المفضلة</string>
<string name="title_blocks">المستخدمون المحظورون</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s عزز</string>
<string name="status_sensitive_media_title">محتوى حساس</string>
<string name="status_sensitive_media_directions">اضغط للعرض</string>
<string name="status_content_warning_show_more">اعرض أكثر</string>
<string name="status_content_warning_show_less">اعرض أقل</string>
<string name="footer_end_of_statuses">نهاية الحالات</string>
<string name="footer_end_of_notifications">نهاية الاشعارات</string>
<string name="footer_end_of_accounts">نهاية الحسابات</string>
<!-- <string name="footer_empty">There are no toots here so far. Pull down to refresh!</string> -->
<string name="notification_reblog_format">%s عزز تبويقك</string>
<string name="notification_favourite_format">%s أعجب بتبويقك</string>
<string name="notification_follow_format">%s يتبعك</string>
<string name="report_username_format">أبلغ عن @%s</string>
<string name="report_comment_hint">تعليقات إضافية ؟</string>
<string name="action_reply">أجب</string>
<string name="action_reblog">عزز</string>
<string name="action_favourite">تفضيل</string>
<string name="action_more">المزيد</string>
<string name="action_compose">حرر</string>
<string name="action_login">التسجيل بواسطة ماستدون</string>
<string name="action_logout">خروج</string>
<string name="action_follow">إتبع</string>
<string name="action_unfollow">إلغاء التتبع</string>
<string name="action_block">حضر</string>
<string name="action_unblock">إلغاء الحظر</string>
<string name="action_report">أبلغ</string>
<string name="action_delete">إحذف</string>
<string name="action_send">تبويق</string>
<string name="action_send_public">بَوِّق</string>
<string name="action_retry">إعادة المحاولة</string>
<!-- <string name="action_mark_sensitive">Mark media sensitive</string> -->
<string name="action_hide_text">اخفي النص وراء تحذير</string>
<string name="action_ok">موافق</string>
<string name="action_cancel">إلغاء</string>
<string name="action_close">إغلاق</string>
<string name="action_back">عودة</string>
<string name="action_view_profile">الملف الشخصي</string>
<string name="action_view_preferences">التفضيلات</string>
<string name="action_view_favourites">المفضلة</string>
<string name="action_view_blocks">المستخدمون المحظورون</string>
<string name="action_view_thread">الخيط</string>
<string name="action_view_media">وسائط</string>
<string name="action_open_in_web">إفتح في متصفح</string>
<string name="action_submit">ارسل</string>
<string name="action_photo_pick">إضافة وسائط</string>
<!-- <string name="action_photo_take">Take photo</string> -->
<string name="action_share">شارك</string>
<string name="action_mute">أكتم</string>
<string name="action_unmute">إلغاء الكتم</string>
<string name="action_mention">أذكر</string>
<string name="toggle_nsfw">NSFW</string>
<string name="action_compose_options">خيارات</string>
<string name="action_open_drawer">إفتح الدرج</string>
<string name="action_clear">إمسح</string>
<!-- <string name="action_save">Save</string> -->
<!-- <string name="action_edit_profile">Edit profile</string> -->
<string name="send_status_link_to">شارك رابط التبويق إلى ...</string>
<!-- <string name="send_status_content_to">Share toot to…</string> -->
<string name="search">ابحث عن حسابات ...</string>
<string name="confirmation_send">بَوِّق</string>
<string name="confirmation_reported">تم الإرسال !</string>
<string name="hint_domain">أي سيرفر ؟</string>
<string name="hint_compose">ما الجديد ؟</string>
<string name="hint_content_warning">تحذير عن المحتوى</string>
<!-- <string name="hint_display_name">Display name</string> -->
<!-- <string name="hint_note">Bio</string> -->
<!-- <string name="label_avatar">Avatar</string> -->
<!-- <string name="label_header">Header</string> -->
<string name="link_whats_an_instance">ماذا نعني بمثيل الخادم ؟</string>
<!-- <string name="dialog_whats_an_instance">The address or domain of any instance can be entered -->
<!-- here, such as mastodon.social, icosahedron.website, social.tchncs.de, and -->
<!-- <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">more!</a> -->
<!-- \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to -->
<!-- join and create an account there.\n\nAn instance is a single place where your account is -->
<!-- hosted, but you can easily communicate with and follow folks on other instances as though -->
<!-- you were on the same site. -->
<!-- \n\nMore info can be found at <a href="https://mastodon.social/about">mastodon.social</a>. -->
<!-- </string> -->
<string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string>
<string name="dialog_message_uploading_media">جاري الرفع ...</string>
<!-- <string name="dialog_download_image">Download</string> -->
<!-- <string name="visibility_public">Public: Post to public timelines</string> -->
<!-- <string name="visibility_unlisted">Unlisted: Do not show in public timelines</string> -->
<!-- <string name="visibility_private">Private: Post to followers only</string> -->
<!-- <string name="visibility_direct">Direct: Post to mentioned users only</string> -->
<string name="pref_title_notification_settings">الاشعارات</string>
<string name="pref_title_edit_notification_settings">تعديل الاشعارات</string>
<string name="pref_title_notifications_enabled">دفع الإخطارات</string>
<string name="pref_title_notification_alerts">التنبيهات</string>
<string name="pref_title_notification_alert_sound">إعلام بالصوت</string>
<string name="pref_title_notification_alert_vibrate">إعلام بالاهتزار</string>
<string name="pref_title_notification_alert_light">إعلام بالضوء</string>
<string name="pref_title_notification_filters">أخطرني عندما</string>
<string name="pref_title_notification_filter_mentions">يشار إلي</string>
<string name="pref_title_notification_filter_follows">يتبعني أحد</string>
<string name="pref_title_notification_filter_reblogs">تعزز وتدفع منشوراتي</string>
<string name="pref_title_notification_filter_favourites">يعجب أحد ما بمنشوراتي</string>
<string name="pref_title_appearance_settings">المظهر</string>
<string name="pref_title_light_theme">إستخدم سمةً فاتحة اللون</string>
<string name="pref_title_browser_settings">المتصفح</string>
<string name="pref_title_custom_tabs">إخفاء زر المتابعة أثناء تمرير الصفحة</string>
<!-- <string name="pref_title_hide_follow_button">Hide follow button while scrolling</string> -->
<string name="notification_mention_format">%s أشار إليك</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s و %4$d أخرى</string>
<string name="notification_summary_medium">%1$s, %2$s, و %3$s</string>
<string name="notification_summary_small">%1$s و %2$s</string>
<string name="notification_title_summary">%d تفاعلات جديدة</string>
<string name="description_account_locked">حساب مقفل</string>
<!-- <string name="status_share_content">Share content of toot</string> -->
<!-- <string name="status_share_link">Share link to toot</string> -->
</resources>

@ -1,25 +1,35 @@
<resources>
<string name="error_generic">Ein Fehler ist aufgetreten.</string>
<!-- <string name="error_empty">This cannot be empty.</string> -->
<string name="error_invalid_domain">Ungültige Domain angegeben</string>
<string name="error_failed_app_registration">Diese App konnte sich auf dem Server nicht authentifizieren.</string>
<string name="error_no_web_browser_found">Kein Webbrowser gefunden.</string>
<string name="error_authorization_unknown">Ein undefinierbarer Autorisierungsfehler ist aufgetreten.</string>
<string name="error_authorization_denied">Autorisierung fehlgeschlagen.</string>
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
<string name="error_compose_character_limit">Der Beitrag ist zu lang!</string>
<string name="error_media_upload_size">Die Datei muss kleiner als 4MB sein.</string>
<string name="error_media_upload_type">Dieser Dateityp darf nicht hochgeladen werden.</string>
<string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string>
<string name="error_media_upload_permission">Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.</string>
<string name="error_media_download_permission">Eine Berechtigung wird zum Speichern des Mediums benötigt.</string>
<string name="error_media_upload_image_or_video">Bilder und Videos können beide nicht an den Beitrag angehängt werden.</string>
<string name="error_media_upload_sending">Die Mediendatei konnte nicht hochgeladen werden.</string>
<string name="error_report_too_few_statuses">Mindestens ein Beitrag muss berichtet werden.</string>
<string name="title_home">Start</string>
<string name="title_notifications">Benachrichtigungen</string>
<string name="title_public_local">Lokal</string>
<string name="title_public_federated">Föderiert</string>
<string name="title_thread">Unterhaltung</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Beiträge</string>
<string name="title_follows">Folgt</string>
<string name="title_followers">Folgende</string>
<string name="title_favourites">Favoriten</string>
<string name="title_blocks">Blockierte Nutzer</string>
<string name="title_blocks">Blockierte Accounts</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s teilte</string>
@ -31,6 +41,7 @@
<string name="footer_end_of_statuses">Ende des Beitrages</string>
<string name="footer_end_of_notifications">Ende der Benachrichtigungen</string>
<string name="footer_end_of_accounts">Ende der Accounts</string>
<!-- <string name="footer_empty">There are no toots here so far. Pull down to refresh!</string> -->
<string name="notification_reblog_format">%s teilte deinen Beitrag</string>
<string name="notification_favourite_format">%s favorisierte deinen Beitrag</string>
@ -39,6 +50,10 @@
<string name="report_username_format">\@%s melden</string>
<string name="report_comment_hint">Irgendwelche Anmerkungen?</string>
<string name="action_reply">Antworten</string>
<string name="action_reblog">Teilen</string>
<string name="action_favourite">Favorisieren</string>
<string name="action_more">Mehr</string>
<string name="action_compose">Schreiben</string>
<string name="action_login">Anmelden mit Mastodon</string>
<string name="action_logout">Ausloggen</string>
@ -61,16 +76,25 @@
<string name="action_view_preferences">Einstellungen</string>
<string name="action_view_favourites">Favoriten</string>
<string name="action_view_blocks">Blockierte Nutzer</string>
<string name="action_view_thread">Thread</string>
<string name="action_view_media">Medien</string>
<string name="action_open_in_web">Im Browser öffnen</string>
<string name="action_submit">Senden</string>
<string name="action_photo_pick">Füge Medien hinzu</string>
<!-- <string name="action_photo_take">Take photo</string> -->
<string name="action_share">Teilen</string>
<string name="action_mute">Stummschalten</string>
<string name="action_unmute">Lautschalten</string>
<string name="action_mention">Erwähnen</string>
<string name="toggle_nsfw">NSFW</string>
<string name="action_compose_options">Einstellungen</string>
<string name="action_open_drawer">Drawer öffnen</string>
<string name="action_clear">Löschen</string>
<!-- <string name="action_save">Save</string> -->
<!-- <string name="action_edit_profile">Edit profile</string> -->
<string name="send_status_to">Teile Toot-URL zu…</string>
<string name="send_status_link_to">Teile Toot-URL zu…</string>
<!-- <string name="send_status_content_to">Share toot to…</string> -->
<string name="search">Suche Accounts…</string>
@ -80,6 +104,11 @@
<string name="hint_domain">Welche Instanz?</string>
<string name="hint_compose">Was passiert gerade?</string>
<string name="hint_content_warning">Inhaltswarnung</string>
<!-- <string name="hint_display_name">Display name</string> -->
<!-- <string name="hint_note">Bio</string> -->
<!-- <string name="label_avatar">Avatar</string> -->
<!-- <string name="label_header">Header</string> -->
<string name="link_whats_an_instance">Was ist eine Instanz?</string>
@ -91,35 +120,38 @@
</string>
<string name="dialog_title_finishing_media_upload">Stelle Medienupload fertig</string>
<string name="dialog_message_uploading_media">Lade hoch…</string>
<string name="dialog_download_image">Herunterladen</string>
<string name="visibility_public">Öffentlich sichtbar</string>
<string name="visibility_unlisted">Öffentlich sichtbar, aber nicht in der öffentlichen Timeline</string>
<string name="visibility_private">Nur für Follower und Erwähnte sichtbar</string>
<!-- <string name="visibility_direct">Direct: Post to mentioned users only</string> -->
<string name="pref_title_notification_settings">Benachrichtigungen</string>
<string name="pref_title_edit_notification_settings">Benachrichtigungseinstellungen</string>
<string name="pref_title_notifications_enabled">Push-Benachrichtigungen</string>
<string name="pref_title_notification_alerts">Benachrichtigungen</string>
<string name="pref_title_notification_alert_sound">Benachrichtige mit Sound</string>
<string name="pref_title_notification_alert_vibrate">Benachrichtige mit Vibration</string>
<string name="pref_title_notification_alert_light">Benachrichtige mit Licht</string>
<string name="pref_title_notification_filters">Benachrichtigen wenn</string>
<string name="pref_title_notification_filter_mentions">Ich erwähnt werde</string>
<string name="pref_title_notification_filter_follows">Mir jemand folgt</string>
<string name="pref_title_notification_filter_reblogs">Jemand meine Posts teilt</string>
<string name="pref_title_notification_filter_favourites">Jemandem meine Posts gefallen</string>
<string name="pref_title_appearance_settings">Aussehen</string>
<string name="pref_title_light_theme">Benutze helles Theme</string>
<string name="pref_title_browser_settings">Browser</string>
<string name="pref_title_custom_tabs">Öffne Links in der App</string>
<string name="pref_title_hide_follow_button">Verstecke Button bei Bildlauf </string>
<string name="notification_mention_format">%s hat dich erwähnt</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s und %4$d andere</string>
<string name="notification_summary_medium">%1$s, %2$s, und %3$s</string>
<string name="notification_summary_small">%1$s und %2$s</string>
<string name="notification_title_summary">%d neue Interaktionen</string>
<string name="title_public_local">Lokal</string>
<string name="title_public_federated">Föderiert</string>
<string name="pref_title_notification_alerts">Benachrichtigungen</string>
<string name="pref_title_notification_filter_favourites">Jemandem meine Posts gefallen</string>
<string name="pref_title_notification_filters">Benachrichtigen wenn</string>
<string name="pref_title_notification_filter_follows">Mir jemand folgt</string>
<string name="pref_title_notification_filter_mentions">Ich erwähnt werde</string>
<string name="pref_title_notification_filter_reblogs">Jemand meine Posts boostet</string>
<string name="error_authorization_denied">Autorisierung fehlgeschlagen.</string>
<string name="error_generic">Ein Fehler ist aufgetreten.</string>
<string name="error_no_web_browser_found">Kein Webbrowser gefunden.</string>
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
<string name="description_account_locked">Gesperrter Account</string>
<!-- <string name="status_share_content">Share content of toot</string> -->
<!-- <string name="status_share_link">Share link to toot</string> -->
</resources>

@ -1,32 +1,35 @@
<resources>
<string name="error_generic">Une erreur s\'est produite.</string>
<string name="error_generic">Une erreur s’est produite.</string>
<string name="error_empty">Ce champ ne peut pas être vide</string>
<string name="error_invalid_domain">Le domaine est invalide</string>
<string name="error_failed_app_registration">L\'application n\'a pu s\'authentifier avec l\'instance.</string>
<string name="error_failed_app_registration">L’application n’a pu s’authentifier auprès de l’instance.</string>
<string name="error_no_web_browser_found">Impossible de trouver un navigateur web.</string>
<string name="error_authorization_unknown">Une erreur d\'authorisation inconnu s\'est produite.</string>
<string name="error_authorization_unknown">Une erreur d’autorisation inconnue s’est produite.</string>
<string name="error_authorization_denied">Vous ne pouvez pas vous authentifier.</string>
<string name="error_retrieving_oauth_token">Impossible de récupérer le jeton d\'authentification.</string>
<string name="error_retrieving_oauth_token">Impossible de récupérer le jeton dauthentification.</string>
<string name="error_compose_character_limit">Votre pouet est trop long!</string>
<string name="error_media_upload_size">Le fichier doit faire moins de 4Mo.</string>
<string name="error_media_upload_type">Ce type de fichier n\'est pas accepté.</string>
<string name="error_media_upload_type">Ce type de fichier nest pas accepté.</string>
<string name="error_media_upload_opening">Le fichier ne peut être ouvert.</string>
<string name="error_media_upload_permission">Une permision pour lire ce média est requis pour l\'uploader.</string>
<string name="error_media_upload_permission">Une permission pour lire ce média est requise pour le mettre en ligne.</string>
<string name="error_media_download_permission">Permission d’enregistrer le fichier requise.</string>
<string name="error_media_upload_image_or_video">Impossible de mettre une vidéo et une image sur le même pouet.</string>
<string name="error_media_upload_sending">Ce média ne peut être uploadé.</string>
<string name="error_media_upload_sending">Ce média ne peut être mis en ligne.</string>
<string name="error_report_too_few_statuses">Au moins un pouet a été reporté.</string>
<string name="title_home">Accueil</string>
<string name="title_notifications">Notifications</string>
<string name="title_public_local">Local</string>
<string name="title_public_federated">Fédéré</string>
<string name="title_thread">Thread</string>
<string name="title_thread">Fil</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Pouets</string>
<string name="title_follows">Follows</string>
<string name="title_followers">Followers</string>
<string name="title_follows">Abonnements</string>
<string name="title_followers">Abonnés</string>
<string name="title_favourites">Favoris</string>
<string name="title_blocks">Utilisateur bloqués</string>
<string name="title_blocks">Utilisateurs bloqués</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosté</string>
@ -38,26 +41,31 @@
<string name="footer_end_of_statuses">fin du pouet</string>
<string name="footer_end_of_notifications">fin des notifications</string>
<string name="footer_end_of_accounts">fin des comptes</string>
<string name="footer_empty">Il n’y a pas encore de pouets ici. Glissez vers le bas pour actualiser !</string>
<string name="notification_reblog_format">%s à boosté votre pouet</string>
<string name="notification_favourite_format">%s à ajouter votre pouet en favoris</string>
<string name="notification_reblog_format">%s a boosté votre pouet</string>
<string name="notification_favourite_format">%s a ajouté votre pouet dans ses favoris</string>
<string name="notification_follow_format">%s vous suit</string>
<string name="report_username_format">Signaler @%s</string>
<string name="report_comment_hint">Plus de commentaire ?</string>
<string name="report_comment_hint">Davantage de commentaires ?</string>
<string name="action_compose">Écrire</string>
<string name="action_reply">Répondre</string>
<string name="action_reblog">Booster</string>
<string name="action_favourite">Favori</string>
<string name="action_more">Plus</string>
<string name="action_compose">Répondre</string>
<string name="action_login">Se connecter avec Mastodon</string>
<string name="action_logout">Deconnexion</string>
<string name="action_follow">Follow</string>
<string name="action_unfollow">Unfollow</string>
<string name="action_logout">Déconnexion</string>
<string name="action_follow">Suivre</string>
<string name="action_unfollow">Ne plus suivre</string>
<string name="action_block">Bloquer</string>
<string name="action_unblock">Débloquer</string>
<string name="action_report">Signaler</string>
<string name="action_delete">Supprimer</string>
<string name="action_send">POUET</string>
<string name="action_send_public">POUET!</string>
<string name="action_retry">Ré-essayer</string>
<string name="action_send_public">POUET !</string>
<string name="action_retry">Essayer encore</string>
<string name="action_mark_sensitive">Définir le média comme sensible</string>
<string name="action_hide_text">Masquer le texte par une mise en garde</string>
<string name="action_ok">Ok</string>
@ -65,63 +73,86 @@
<string name="action_close">Fermer</string>
<string name="action_back">Retour</string>
<string name="action_view_profile">Profil</string>
<string name="action_view_preferences">Préferences</string>
<string name="action_view_preferences">Préférences</string>
<string name="action_view_favourites">Favoris</string>
<string name="action_view_blocks">Utilisateurs bloqués</string>
<string name="action_view_thread">Fil</string>
<string name="action_view_media">Média</string>
<string name="action_open_in_web">Ouvrir avec votre navigateur</string>
<string name="action_submit">Envoyer</string>
<string name="action_photo_pick">Ajouter un média</string>
<string name="action_photo_take">Prendre une photo</string>
<string name="action_share">Partager</string>
<string name="action_mute">Rendre muet</string>
<string name="action_unmute">Redonner la parole</string>
<string name="action_mention">Mention</string>
<string name="toggle_nsfw">NSFW</string>
<string name="action_compose_options">Option</string>
<string name="action_open_drawer">Ouvrir le menu</string>
<string name="action_clear">Nettoyer</string>
<string name="action_save">Sauvegarder</string>
<string name="action_edit_profile">Modifier le profil</string>
<string name="send_status_to">Partager l\'URL de votre pouet avec…</string>
<string name="send_status_link_to">Partager l’URL de votre pouet avec…</string>
<string name="send_status_content_to">Partager le pouet avec…</string>
<string name="search">Rechercher un compte…</string>
<string name="confirmation_send">Toot!</string>
<string name="confirmation_reported">Envoyer!</string>
<string name="confirmation_send">Pouet !</string>
<string name="confirmation_reported">Envoyé !</string>
<string name="hint_domain">Quelle instance?</string>
<string name="hint_domain">Quelle instance ?</string>
<string name="hint_compose">Quoi de neuf ?</string>
<string name="hint_content_warning">Contenu mis en garde</string>
<string name="hint_content_warning">Contenu sensible</string>
<string name="hint_display_name">Afficher le nom</string>
<string name="hint_note">Bio</string>
<string name="label_avatar">Avatar</string>
<string name="label_header">En-tête</string>
<string name="link_whats_an_instance">Qu\'est ce qu\'une instance?</string>
<string name="link_whats_an_instance">Qu’est ce qu’une instance ?</string>
<string name="dialog_whats_an_instance">L\'adresse ou le domaine d\'une instance peut être entré
Ici, comme mastodon.social, icosahedron.website, social.tchncs.de, et
<a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">plus (en anglais)!</a>
\n\nSi vous n\'avez pas de compte, Vous pouvez entrer le nom de l\'instance que vous voulez rejoindre et créer un compte ici.\n\n Une instance est l\'endroit où votre compte est
stocké, mais vous pouvez facilement communiquer et suivre d\'autre personne sur d\'autre instance bien que vous soyez sur le même site
\n\nPlus d\'info <a href="https://mastodon.social/about">mastodon.social</a> (anglais).
<string name="dialog_whats_an_instance">L’adresse ou le domaine d’une instance peut être entré
ici, comme mastodon.social, icosahedron.website, social.tchncs.de,
<a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">ou autre</a> (en anglais) !
\n\nSi vous n’avez pas de compte, vous pouvez entrer le nom de l’instance que vous voulez rejoindre et créer un compte ici.\n\nUne instance est l’endroit où votre compte est
stocké, mais vous pouvez facilement communiquer et suivre d’autres personnes sur d’autres instances bien que vous soyez sur le même site
\n\nPlus dinfo <a href="https://mastodon.social/about">mastodon.social</a> (anglais).
</string>
<string name="dialog_title_finishing_media_upload">Média uploadé avec succès</string>
<string name="dialog_message_uploading_media">Téléversement…</string>
<string name="dialog_title_finishing_media_upload">Média mis en ligne avec succès</string>
<string name="dialog_message_uploading_media">Mise en ligne…</string>
<string name="dialog_download_image">Télécharger</string>
<string name="visibility_public">Tout le monde peut voir</string>
<string name="visibility_unlisted">Tout le monde peut voir, mais cela ne sera pas listé sur votre timeline public</string>
<string name="visibility_private">Uniquement les followers et les mentionnés peuvent voir</string>
<string name="visibility_public">Public: Afficher dans les fils publics</string>
<string name="visibility_unlisted">Non-listé: Ne pas afficher dans les fils publics</string>
<string name="visibility_private">Privé: N’afficher que pour vos abonné⋅e⋅s</string>
<string name="visibility_direct">Direct: N’afficher que pour les personnes mentionnées</string>
<string name="pref_title_notification_settings">Notifications</string>
<string name="pref_title_edit_notification_settings">Modifier la notification</string>
<string name="pref_title_notifications_enabled">Notifications push</string>
<string name="pref_title_notification_alerts">Alertes</string>
<string name="pref_title_notification_alert_sound">Sonner pour notifier</string>
<string name="pref_title_notification_alert_sound">Émettre un son pour notifier</string>
<string name="pref_title_notification_alert_vibrate">Vibrer pour notifier</string>
<string name="pref_title_notification_alert_light">Notifier avec une LED</string>
<string name="pref_title_notification_filters">Me notifier quand</string>
<string name="pref_title_notification_filter_mentions">mentionné</string>
<string name="pref_title_notification_filter_follows">suivit</string>
<string name="pref_title_notification_filter_reblogs">mes pouets boostés</string>
<string name="pref_title_notification_filter_favourites">mes pouets mis en favoris</string>
<string name="pref_title_notification_filter_follows">suivi</string>
<string name="pref_title_notification_filter_reblogs">mes pouets sont boostés</string>
<string name="pref_title_notification_filter_favourites">mes pouets sont mis en favoris</string>
<string name="pref_title_appearance_settings">Apparence</string>
<string name="pref_title_light_theme">Utiliser le thème clair</string>
<string name="pref_title_browser_settings">Navigateur</string>
<string name="pref_title_custom_tabs">Utiliser le navigateur intégré</string>
<string name="pref_title_hide_follow_button">Cacher le bouton de suivi lors du défilement</string>
<string name="notification_mention_format">%s vous ont mentionnés</string>
<string name="notification_mention_format">%s vous ont mentionné</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d plus</string>
<string name="notification_summary_medium">%1$s, %2$s, et %3$s</string>
<string name="notification_summary_small">%1$s et %2$s</string>
<string name="notification_title_summary">%d nouvelles interactions</string>
<string name="description_account_locked">Compte bloqué</string>
<string name="status_share_content">Partager le contenu du pouet</string>
<string name="status_share_link">Partager le lien du pouet</string>
</resources>

@ -0,0 +1,164 @@
<resources>
<string name="error_generic">エラーが発生しました。</string>
<string name="error_empty">本文なしでは投稿できません。</string>
<string name="error_invalid_domain">無効なドメインです</string>
<string name="error_failed_app_registration">そのインスタンスでの認証に失敗しました。</string>
<string name="error_no_web_browser_found">ウェブブラウザが見つかりませんでした。</string>
<string name="error_authorization_unknown">不明な承認エラーが発生しました。</string>
<string name="error_authorization_denied">承認が拒否されました。</string>
<string name="error_retrieving_oauth_token">ログイントークンの取得に失敗しました。</string>
<string name="error_compose_character_limit">投稿文が長すぎます!</string>
<string name="error_media_upload_size">ファイルは4MB未満にしてください。</string>
<string name="error_media_upload_type">その形式のファイルはアップロードできません。</string>
<string name="error_media_upload_opening">ファイルを開けませんでした。</string>
<string name="error_media_upload_permission">メディアの読み取り許可が必要です。</string>
<string name="error_media_download_permission">メディアの書き込み許可が必要です。</string>
<string name="error_media_upload_image_or_video">画像と動画を同時に投稿することはできません。</string>
<string name="error_media_upload_sending">アップロードに失敗しました。</string>
<string name="error_report_too_few_statuses">少なくとも1つの投稿を報告してください。</string>
<string name="title_home">ホーム</string>
<string name="title_notifications">通知</string>
<string name="title_public_local">ローカル</string>
<string name="title_public_federated">連合</string>
<string name="title_thread">スレッド</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">投稿</string>
<string name="title_follows">フォロー</string>
<string name="title_followers">フォロワー</string>
<string name="title_favourites">お気に入り</string>
<string name="title_mutes">ミュートしたユーザー</string>
<string name="title_blocks">ブロックしたユーザー</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%sさんがブーストしました</string>
<string name="status_sensitive_media_title">不適切なコンテンツ</string>
<string name="status_sensitive_media_directions">タップして表示</string>
<string name="status_content_warning_show_more">続きを表示</string>
<string name="status_content_warning_show_less">続きを隠す</string>
<string name="footer_end_of_statuses">これ以降に投稿はありません</string>
<string name="footer_end_of_notifications">これ以降に通知はありません</string>
<string name="footer_end_of_accounts">これ以降にアカウントはありません</string>
<string name="footer_empty">現在トゥートはありません。更新するにはプルダウンしてください!</string>
<string name="notification_reblog_format">%sさんがトゥートをブーストしました</string>
<string name="notification_favourite_format">%sさんがトゥートをお気に入りに登録しました</string>
<string name="notification_follow_format">%sさんにフォローされました</string>
<string name="report_username_format">\@%sさんを通報</string>
<string name="report_comment_hint">コメント</string>
<string name="action_reply">返信</string>
<string name="action_reblog">ブースト</string>
<string name="action_favourite">お気に入り</string>
<string name="action_more">その他</string>
<string name="action_compose">新規投稿</string>
<string name="action_login">Mastodonでログイン</string>
<string name="action_logout">ログアウト</string>
<string name="action_follow">フォローする</string>
<string name="action_unfollow">フォロー解除</string>
<string name="action_block">ブロック</string>
<string name="action_unblock">ブロック解除</string>
<string name="action_report">通報</string>
<string name="action_delete">削除</string>
<string name="action_send">トゥート</string>
<string name="action_send_public">トゥート!</string>
<string name="action_retry">再試行</string>
<string name="action_mark_sensitive">不適切なコンテンツとして設定</string>
<string name="action_hide_text">投稿文を注意書きで隠す</string>
<string name="action_ok">OK</string>
<string name="action_cancel">キャンセル</string>
<string name="action_close">閉じる</string>
<string name="action_back">戻る</string>
<string name="action_view_profile">プロフィール</string>
<string name="action_view_preferences">設定</string>
<string name="action_view_favourites">お気に入り</string>
<string name="action_view_mutes">ミュートしたユーザー</string>
<string name="action_view_blocks">ブロックしたユーザー</string>
<string name="action_view_thread">スレッド</string>
<string name="action_view_media">メディア</string>
<string name="action_open_in_web">ブラウザで開く</string>
<string name="action_submit">決定</string>
<string name="action_photo_pick">メディアを追加</string>
<string name="action_photo_take">写真を撮る</string>
<string name="action_share">共有</string>
<string name="action_mute">ミュート</string>
<string name="action_unmute">ミュート解除</string>
<string name="action_mention">返信</string>
<string name="toggle_nsfw">NSFW</string>
<string name="action_compose_options">オプション</string>
<string name="action_open_drawer">メニューを開く</string>
<string name="action_clear">消去</string>
<string name="action_save">保存</string>
<string name="action_edit_profile">プロフィールを編集</string>
<string name="action_undo">取り消す</string>
<string name="send_status_link_to">トゥートのURLを共有…</string>
<string name="send_status_content_to">トゥートを共有…</string>
<string name="search">ユーザーを検索…</string>
<string name="confirmation_send">トゥート!</string>
<string name="confirmation_reported">送信しました!</string>
<string name="confirmation_unblocked">ブロックを解除しました</string>
<string name="confirmation_unmuted">ミュートを解除しました</string>
<string name="hint_domain">どのインスタンス?</string>
<string name="hint_compose">今なにしてる?</string>
<string name="hint_content_warning">注意書き</string>
<string name="hint_display_name">表示名</string>
<string name="hint_note">プロフィール</string>
<string name="label_avatar">アイコン</string>
<string name="label_header">ヘッダー</string>
<string name="link_whats_an_instance">インスタンスとは?</string>
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.net や
<a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md">その他</a>
のような、あらゆるインスタンスのアドレスやドメインを入力できます。
\n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで
そのインスタンスにアカウントを作成できます。\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、
他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
\n\nさらに詳しい情報は<a href="https://mastodon.social/about">mastodon.social</a>でご覧いただけます。
</string>
<string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string>
<string name="dialog_message_uploading_media">アップロード中…</string>
<string name="dialog_download_image">ダウンロード</string>
<string name="visibility_public">公開:公開タイムラインに投稿する</string>
<string name="visibility_unlisted">未収載:公開タイムラインには表示しない</string>
<string name="visibility_private">非公開:フォロワーだけに公開</string>
<string name="visibility_direct">ダイレクト:返信先のユーザーだけに公開</string>
<string name="pref_title_notification_settings">通知</string>
<string name="pref_title_edit_notification_settings">通知を設定</string>
<string name="pref_title_notifications_enabled">プッシュ通知</string>
<string name="pref_title_notification_alerts">通知時の動作</string>
<string name="pref_title_notification_alert_sound">着信音を鳴らす</string>
<string name="pref_title_notification_alert_vibrate">バイブレーションする</string>
<string name="pref_title_notification_alert_light">通知ランプ</string>
<string name="pref_title_notification_filters">通知の種類</string>
<string name="pref_title_notification_filter_mentions">返信</string>
<string name="pref_title_notification_filter_follows">フォロー</string>
<string name="pref_title_notification_filter_reblogs">投稿がブーストされた</string>
<string name="pref_title_notification_filter_favourites">投稿がお気に入りに登録された</string>
<string name="pref_title_appearance_settings">表示</string>
<string name="pref_title_light_theme">明るいテーマを使用</string>
<string name="pref_title_browser_settings">ブラウザ</string>
<string name="pref_title_custom_tabs">Chrome Custom Tabsを使用する</string>
<string name="pref_title_hide_follow_button">スクロール中はフォローボタンを隠す</string>
<string name="notification_mention_format">%sさんが返信しました</string>
<string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string>
<string name="notification_summary_medium">%1$sさん、%2$sさん、%3$sさん</string>
<string name="notification_summary_small">%1$sさん、%2$sさん</string>
<string name="notification_title_summary">%d件の新しい通知</string>
<string name="description_account_locked">非公開アカウント</string>
<string name="status_share_content">トゥートの内容を共有</string>
<string name="status_share_link">トゥートへのリンクを共有</string>
</resources>

@ -0,0 +1,154 @@
<resources>
<string name="error_generic">Er ging iets verkeerd.</string>
<string name="error_empty">Dit kan niet leeg blijven.</string>
<string name="error_invalid_domain">Ongeldige domeinnaam ingevoerd</string>
<string name="error_failed_app_registration">Authenticatie met die server is mislukt.</string>
<string name="error_no_web_browser_found">Kon geen webbrowser vinden om te gebruiken.</string>
<string name="error_authorization_unknown">Onbekende autorisatiefout.</string>
<string name="error_authorization_denied">Autorisatie werd geweigerd.</string>
<string name="error_retrieving_oauth_token">Verkrijgen van inlogsleutel is mislukt.</string>
<string name="error_compose_character_limit">Tekst van deze toot is te lang!</string>
<string name="error_media_upload_size">Bestand moet kleiner zijn dan 4MB.</string>
<string name="error_media_upload_type">Bestandstype kan niet worden geüpload.</string>
<string name="error_media_upload_opening">Bestand kan niet worden geopend.</string>
<string name="error_media_upload_permission">Toestemming nodig om media te kunnen lezen.</string>
<!-- <string name="error_media_download_permission">Permission to store media is required.</string> -->
<string name="error_media_upload_image_or_video">Afbeeldingen en video\'s kunnen niet allebei aan dezelfde toot worden toegevoegd.</string>
<string name="error_media_upload_sending">Uploaden mislukt.</string>
<string name="error_report_too_few_statuses">Tenminste één toot moet worden gerapporteerd.</string>
<string name="title_home">Jouw tijdlijn</string>
<string name="title_notifications">Meldingen</string>
<string name="title_public_local">Lokale tijdlijn</string>
<string name="title_public_federated">Globale tijdlijn</string>
<string name="title_thread">Conversatie</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Toots</string>
<string name="title_follows">Volgt</string>
<string name="title_followers">Volgers</string>
<string name="title_favourites">Favorieten</string>
<string name="title_blocks">Geblokkeerde</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boostte</string>
<string name="status_sensitive_media_title">Gevoelige inhoud</string>
<string name="status_sensitive_media_directions">Klik om te zien</string>
<string name="status_content_warning_show_more">Meer tonen</string>
<string name="status_content_warning_show_less">Minder tonen</string>
<string name="footer_end_of_statuses">einde van alle toots</string>
<string name="footer_end_of_notifications">einde van alle meldingen</string>
<string name="footer_end_of_accounts">einde van alle accounts</string>
<string name="footer_empty">Hier bevinden zich nog geen toots. Trek omlaag om te vernieuwen!</string>
<string name="notification_reblog_format">%s boostte jouw toot</string>
<string name="notification_favourite_format">%s markeerde jouw toot als favoriet</string>
<string name="notification_follow_format">%s volgt jou</string>
<string name="report_username_format">Rapporteer @%s</string>
<string name="report_comment_hint">Additional comments?</string>
<string name="action_reply">Reageren</string>
<string name="action_reblog">Boost</string>
<string name="action_favourite">Favoriet</string>
<string name="action_more">Meer</string>
<string name="action_compose">Schrijven</string>
<string name="action_login">Inloggen bij Mastodon</string>
<string name="action_logout">Uitloggen</string>
<string name="action_follow">Volgen</string>
<string name="action_unfollow">Ontvolgen</string>
<string name="action_block">Blokkeren</string>
<string name="action_unblock">Deblokkeren</string>
<string name="action_report">Rapporteren</string>
<string name="action_delete">Verwijderen</string>
<string name="action_send">Toot</string>
<string name="action_send_public">TOOT!</string>
<string name="action_retry">Opnieuw proberen</string>
<string name="action_mark_sensitive">Media als gevoelig markeren</string>
<string name="action_hide_text">Tekst achter waarschuwing verbergen</string>
<string name="action_ok">OK</string>
<string name="action_cancel">Annuleren</string>
<string name="action_close">Sluiten</string>
<string name="action_back">Terug</string>
<string name="action_view_profile">Profiel</string>
<string name="action_view_preferences">Voorkeuren</string>
<string name="action_view_favourites">Favorieten</string>
<string name="action_view_blocks">Geblokkeerde gebruikers</string>
<string name="action_view_thread">Conversatie</string>
<string name="action_view_media">Media</string>
<string name="action_open_in_web">Open in webbrowser</string>
<string name="action_submit">Opslaan</string>
<string name="action_photo_pick">Media toevoegen</string>
<string name="action_share">Delen</string>
<string name="action_mute">Negeren</string>
<string name="action_unmute">Niet meer negeren</string>
<string name="action_mention">Vermelden</string>
<string name="toggle_nsfw">Gevoelig (NSFW)</string>
<string name="action_compose_options">Opties</string>
<string name="action_open_drawer">Menu openen</string>
<string name="action_clear">Leegmaken</string>
<!-- <string name="action_save">Save</string> -->
<!-- <string name="action_edit_profile">Edit profile</string> -->
<string name="send_status_link_to">Deel link van toot met…</string>
<string name="send_status_content_to">Deel toot met…</string>
<string name="search">Zoek accounts…</string>
<string name="confirmation_send">Toot!</string>
<string name="confirmation_reported">Verzenden!</string>
<string name="hint_domain">Welke Mastodon-server?</string>
<string name="hint_compose">Wat wil je kwijt?</string>
<string name="hint_content_warning">Waarschuwingstekst</string>
<!-- <string name="hint_display_name">Display name</string> -->
<!-- <string name="hint_note">Bio</string> -->
<!-- <string name="label_avatar">Avatar</string> -->
<!-- <string name="label_header">Header</string> -->
<string name="link_whats_an_instance">Wat is een Mastodon-server?</string>
<string name="dialog_whats_an_instance">Het adres of domein van elke Mastodon-server kan hier worden ingevoerd, zoals mastodon.social, mastodon.cloud, octodon.social en <a href="https://instances.mastodon.xyz/">veel meer!</a>
\n\nWanneer je nog geen account hebt, kun je de naam van de Mastodon-server waar jij je graag wil registeren invoeren, waarna je daarna daar een account kunt aanmaken.\n\nEen Mastodon-server is een computerserver waar jouw account zich bevindt, maar je kan eenvoudig mensen van andere servers volgen en met ze communiceren, alsof jullie met elkaar op dezelfde website zitten.
\n\nMeer informatie kun je vinden op <a href="https://mastodon.social/about">mastodon.social</a>.
</string>
<string name="dialog_title_finishing_media_upload">Uploaden media wordt voltooid</string>
<string name="dialog_message_uploading_media">Uploaden…</string>
<!-- <string name="dialog_download_image">Download</string> -->
<string name="visibility_public">Openbaar</string>
<string name="visibility_unlisted">Openbaar, maar niet op een openbare tijdlijn tonen</string>
<string name="visibility_private">Alleen aan volgers tonen</string>
<!-- <string name="visibility_direct">Direct: Post to mentioned users only</string> -->
<string name="pref_title_notification_settings">Meldingen</string>
<string name="pref_title_edit_notification_settings">Meldingen bewerken</string>
<string name="pref_title_notifications_enabled">Meldingen pushen</string>
<string name="pref_title_notification_alerts">Waarschuwingen</string>
<string name="pref_title_notification_alert_sound">Geluid</string>
<string name="pref_title_notification_alert_vibrate">Trillen</string>
<string name="pref_title_notification_alert_light">LED</string>
<string name="pref_title_notification_filters">Geef een melding wanneer</string>
<string name="pref_title_notification_filter_mentions">ik werd vermeld</string>
<string name="pref_title_notification_filter_follows">ik werd gevolgd</string>
<string name="pref_title_notification_filter_reblogs">mijn toots werden geboost</string>
<string name="pref_title_notification_filter_favourites">mijn toots favoriet zijn</string>
<string name="pref_title_appearance_settings">Uiterlijk</string>
<string name="pref_title_light_theme">Gebruik het lichte thema</string>
<string name="pref_title_browser_settings">Webrowser</string>
<string name="pref_title_custom_tabs">Gebruik Chrome aangepaste tabbladen</string>
<string name="pref_title_hide_follow_button">Verberg volgknop tijdens scrollen</string>
<string name="notification_mention_format">%s vermeldde jou</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s en %4$d anderen</string>
<string name="notification_summary_medium">%1$s, %2$s en %3$s</string>
<string name="notification_summary_small">%1$s en %2$s</string>
<string name="notification_title_summary">%d nieuwe interacties</string>
<string name="description_account_locked">Geblokkeerde account</string>
<string name="status_share_content">Deel inhoud van toot</string>
<string name="status_share_link">Deel link met toot</string>
</resources>

@ -0,0 +1,159 @@
<resources>
<string name="error_generic">Bir hata oluştu.</string>
<string name="error_empty">Bu alan boş bırakılmaz.</string>
<string name="error_invalid_domain">Girilen etki alanı geçersiz.</string>
<string name="error_failed_app_registration">Kimliği bu sunucuda doğrulayamadı.</string>
<string name="error_no_web_browser_found">Kullanılabilen tarayıcı bulunmadı.</string>
<string name="error_authorization_unknown">Açıklanmayan kimlik doğrulama hata oluştu.</string>
<string name="error_authorization_denied">Kimlik doğrulama reddedildi.</string>
<string name="error_retrieving_oauth_token">Giriş jetonu alınamadı.</string>
<string name="error_compose_character_limit">İleti fazlasıyla uzun!</string>
<string name="error_media_upload_size">Dosya 4MB\'ten küçük olmalı.</string>
<string name="error_media_upload_type">O biçim dosya yüklenmez.</string>
<string name="error_media_upload_opening">O dosya açılamadı.</string>
<string name="error_media_upload_permission">Medya okuma izni gerekiyor.</string>
<string name="error_media_download_permission">Medya kaydetmek için izin gerekiyor.</string>
<string name="error_media_upload_image_or_video">Aynı iletiye kem video hem resim eklenemez.</string>
<string name="error_media_upload_sending">Yükleme başarsız.</string>
<string name="error_report_too_few_statuses">Her bildirme en azından bir iletiyi işaret etmeli.</string>
<string name="title_home">Ana sayfa</string>
<string name="title_notifications">Bildirimler</string>
<string name="title_public_local">Yerel</string>
<string name="title_public_federated">Birleşmiş</string>
<string name="title_thread">Dizi</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">İletiler</string>
<string name="title_follows">Takip edilenler</string>
<string name="title_followers">Takipçiler</string>
<string name="title_favourites">Favoriler</string>
<string name="title_blocks">Engellenmiş kullanıcılar</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s yükseltti</string>
<string name="status_sensitive_media_title">Hasas Medya</string>
<string name="status_sensitive_media_directions">Görmek için taklayın</string>
<string name="status_content_warning_show_more">Daha Fazla Göster</string>
<string name="status_content_warning_show_less">Daha Az Göster</string>
<string name="footer_end_of_statuses">iletilerin sonu</string>
<string name="footer_end_of_notifications">bildirmelerin sonu</string>
<string name="footer_end_of_accounts">hesapların sonu</string>
<string name="footer_empty">Henüz hiç ileti yoktur. Yenilemek için aşağıya çek!</string>
<string name="notification_reblog_format">%s iletini yükseltti</string>
<string name="notification_favourite_format">%s iletini favori etti</string>
<string name="notification_follow_format">%s seni takip etti</string>
<string name="report_username_format">\@%s bildir</string>
<string name="report_comment_hint">Daha fazla yorum?</string>
<string name="action_reply">Yanıtla</string>
<string name="action_reblog">Yükselt</string>
<string name="action_favourite">Favori edin</string>
<string name="action_more">Daha fazla</string>
<string name="action_compose">Oluştur</string>
<string name="action_login">Mastodon ile giriş yap</string>
<string name="action_logout">Çıkış Yap</string>
<string name="action_follow">Takip et</string>
<string name="action_unfollow">Takibi bırak</string>
<string name="action_block">Engelle</string>
<string name="action_unblock">Engeli kaldır</string>
<string name="action_report">Bildir</string>
<string name="action_delete">Sil</string>
<string name="action_send">İLET</string>
<string name="action_send_public">İLET!</string>
<string name="action_retry">Tekrar dene</string>
<string name="action_mark_sensitive">Medyayı hassas olarak etiketle</string>
<string name="action_hide_text">Metini uyarı ile sakla</string>
<string name="action_ok">Tamam</string>
<string name="action_cancel">İptal</string>
<string name="action_close">Kapat</string>
<string name="action_back">Geri</string>
<string name="action_view_profile">Profil</string>
<string name="action_view_preferences">Ayarlar</string>
<string name="action_view_favourites">Favoriler</string>
<string name="action_view_blocks">Engellenmiş kullanıcılar</string>
<string name="action_view_thread">Dizi</string>
<string name="action_view_media">Medya</string>
<string name="action_open_in_web">Tarayıcıda aç</string>
<string name="action_submit">Gönder</string>
<string name="action_photo_pick">Medya ekle</string>
<string name="action_photo_take">Resim çek</string>
<string name="action_share">Paylaş</string>
<string name="action_mute">Sesize al</string>
<string name="action_unmute">Sesizden kaldır</string>
<string name="action_mention">Bahset</string>
<string name="toggle_nsfw">UYARILI</string>
<string name="action_compose_options">Seçenekler</string>
<string name="action_open_drawer">Çekmece aç</string>
<string name="action_clear">Temizle</string>
<string name="action_save">Kaydet</string>
<string name="action_edit_profile">Profili düzelt</string>
<string name="send_status_link_to">İletinin adresini paylaş…</string>
<string name="send_status_content_to">İletiyi paylaş…</string>
<string name="search">Hesaplarda ara…</string>
<string name="confirmation_send">İlet!</string>
<string name="confirmation_reported">İletildi!</string>
<string name="hint_domain">Hangi sunucu?</string>
<string name="hint_compose">Neler oluyor?</string>
<string name="hint_content_warning">İçerik uyarı</string>
<string name="hint_display_name">Görünen ad</string>
<string name="hint_note">Biyo</string>
<string name="label_avatar">Simge</string>
<string name="label_header">Üstlük</string>
<string name="link_whats_an_instance">Sunucu nedir?</string>
<string name="dialog_whats_an_instance">Burada her hangi bir Mastodon sunucusunun adresi
(mastodon.social, icosahedron.website, social.tchncs.de, ve
<a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">daha fazla!</a>) girilebiliri.
\n\nEğer hesabınız henüz yok ise katılmak istediğiniz sunucunun adresini girerek hesap yaratabilirsin.
\n\nHer bir sunucu hesaplar ağırlayan bir yer olur ancak diğer sunucularda bulunan insanlarla
aynı sitede olmuşcasına iletişime geçip takip edebilirsiniz.
\n\nDaha fazla bilgi için <a href="https://mastodon.social/about">mastodon.social</a>.
</string>
<string name="dialog_title_finishing_media_upload">Medya Yükleme Bittiriliyor</string>
<string name="dialog_message_uploading_media">Yükleniyor…</string>
<string name="dialog_download_image">İndir</string>
<string name="visibility_public">Kamu: Herkese açık ve sosyal çizelgelerinde çıkar</string>
<string name="visibility_unlisted">Saklı: Herkese açık ancık sosyal çizelgesinde çıkmaz</string>
<string name="visibility_private">Özel: Sadece takipçiler ve bahsedilenlere açık</string>
<string name="visibility_direct">Direkt: Sadece bahsedilen kullanıcılara açık</string>
<string name="pref_title_notification_settings">Bildirimler</string>
<string name="pref_title_edit_notification_settings">Bildirimleri düzelt</string>
<string name="pref_title_notifications_enabled">Anlık bildirimler</string>
<string name="pref_title_notification_alerts">Uyarılar</string>
<string name="pref_title_notification_alert_sound">Sesle bildir</string>
<string name="pref_title_notification_alert_vibrate">Titremeyle bildir</string>
<string name="pref_title_notification_alert_light">Işığıyla bildir</string>
<string name="pref_title_notification_filters">Beni bildir</string>
<string name="pref_title_notification_filter_mentions">bahsedilince</string>
<string name="pref_title_notification_filter_follows">takip edilince</string>
<string name="pref_title_notification_filter_reblogs">iletilerim yüksetilince</string>
<string name="pref_title_notification_filter_favourites">iletilerim favori edilince</string>
<string name="pref_title_appearance_settings">Görünüş</string>
<string name="pref_title_light_theme">Açık renkli temayı kullan</string>
<string name="pref_title_browser_settings">Tarayacı</string>
<string name="pref_title_custom_tabs">Chrome Özel Şekmelerini Kullan</string>
<string name="pref_title_hide_follow_button">Kaydırırken takip düğmesi gizlensin</string>
<string name="notification_mention_format">%s senden bahsetti</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s ve %4$d daha</string>
<string name="notification_summary_medium">%1$s, %2$s ve %3$s</string>
<string name="notification_summary_small">%1$s ve %2$s</string>
<string name="notification_title_summary">%d yeni etkileşim</string>
<string name="description_account_locked">Kitli Hesap</string>
<string name="status_share_content">İletinin içeriğini paylaş</string>
<string name="status_share_link">İletinin adresini paylaş</string>
</resources>

@ -1,6 +1,7 @@
<resources>
<string name="error_generic">An error occurred.</string>
<string name="error_empty">This cannot be empty.</string>
<string name="error_invalid_domain">Invalid domain entered</string>
<string name="error_failed_app_registration">Failed authenticating with that instance.</string>
<string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string>
@ -12,6 +13,8 @@
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
<string name="error_media_upload_opening">That file could not be opened.</string>
<string name="error_media_upload_permission">Permission to read media is required.</string>
<string name="error_media_download_permission">Permission to store media is required.</string>
<string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same status.</string>
<string name="error_media_upload_sending">The upload failed.</string>
<string name="error_report_too_few_statuses">At least one status must be reported.</string>
@ -26,7 +29,9 @@
<string name="title_follows">Follows</string>
<string name="title_followers">Followers</string>
<string name="title_favourites">Favourites</string>
<string name="title_mutes">Muted users</string>
<string name="title_blocks">Blocked users</string>
<string name="title_follow_requests">Follow Requests</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string>
@ -38,6 +43,7 @@
<string name="footer_end_of_statuses">end of the statuses</string>
<string name="footer_end_of_notifications">end of the notifications</string>
<string name="footer_end_of_accounts">end of the accounts</string>
<string name="footer_empty">There are no toots here so far. Pull down to refresh!</string>
<string name="notification_reblog_format">%s boosted your toot</string>
<string name="notification_favourite_format">%s favourited your toot</string>
@ -71,12 +77,15 @@
<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_mutes">Muted users</string>
<string name="action_view_blocks">Blocked users</string>
<string name="action_view_follow_requests">Follow Requests</string>
<string name="action_view_thread">Thread</string>
<string name="action_view_media">Media</string>
<string name="action_open_in_web">Open in browser</string>
<string name="action_submit">Submit</string>
<string name="action_photo_pick">Add media</string>
<string name="action_photo_take">Take photo</string>
<string name="action_share">Share</string>
<string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</string>
@ -85,17 +94,30 @@
<string name="action_compose_options">Options</string>
<string name="action_open_drawer">Open drawer</string>
<string name="action_clear">Clear</string>
<string name="action_save">Save</string>
<string name="action_edit_profile">Edit profile</string>
<string name="action_undo">Undo</string>
<string name="action_accept">Accept</string>
<string name="action_reject">Reject</string>
<string name="send_status_to">Share toot URL to…</string>
<string name="send_status_link_to">Share toot URL to…</string>
<string name="send_status_content_to">Share toot to…</string>
<string name="search">Search accounts…</string>
<string name="confirmation_send">Toot!</string>
<string name="confirmation_reported">Sent!</string>
<string name="confirmation_unblocked">User unblocked</string>
<string name="confirmation_unmuted">User unmuted</string>
<string name="hint_domain">Which instance?</string>
<string name="hint_compose">What\'s happening?</string>
<string name="hint_content_warning">Content warning</string>
<string name="hint_display_name">Display name</string>
<string name="hint_note">Bio</string>
<string name="label_avatar">Avatar</string>
<string name="label_header">Header</string>
<string name="link_whats_an_instance">What\'s an instance?</string>
@ -110,10 +132,12 @@
</string>
<string name="dialog_title_finishing_media_upload">Finishing Media Upload</string>
<string name="dialog_message_uploading_media">Uploading…</string>
<string name="dialog_download_image">Download</string>
<string name="visibility_public">Everyone can view</string>
<string name="visibility_unlisted">Everyone can view, but not on public timelines</string>
<string name="visibility_private">Only followers and mentions can view</string>
<string name="visibility_public">Public: Post to public timelines</string>
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>
<string name="visibility_private">Private: Post to followers only</string>
<string name="visibility_direct">Direct: Post to mentioned users only</string>
<string name="pref_title_notification_settings">Notifications</string>
<string name="pref_title_edit_notification_settings">Edit Notifications</string>
@ -140,5 +164,6 @@
<string name="notification_title_summary">%d new interactions</string>
<string name="description_account_locked">Locked Account</string>
<string name="status_share_content">Share content of toot</string>
<string name="status_share_link">Share link to toot</string>
</resources>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" />
</paths>
Loading…
Cancel
Save