@ -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. |
@ -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); |
||||
} |
||||
} |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 14 KiB |
@ -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); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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(""); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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> |
After Width: | Height: | Size: 7.6 KiB |
@ -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,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> |
@ -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> |
@ -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> |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
@ -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> |
@ -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> |