ComposeActivity improvements (#548)
* do not add media urls to status text * add scrolling to content * add arrow icon and animation to replying-to toggle * remove unnecessary compose_button_colors.xml * improve toot button * improve bottom bar, add bottom sheet for compose options, dedicated cw button * fix crash on Android < API 21 * move media picking from dialog to bottom sheet * add small style tootbutton * fix colors/button background for light theme * add icons to media chose bottom sheet * improve hide media button, delete unused styles * fix crash on dev build when taking photo * consolidate drawables * consolidate strings and ids, add tooltips to buttons * allow media only toots * change error message to show max size of upload correctly * fix button color * add emoji * code cleanup * Merge branch 'master' into compose_activity_refactoring # Conflicts: # app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java * fix hidden snackbar * improve hint text color * add SendTootService * fix timeline refreshing * toot saving and error handling for sendtootservice * restructure some code * convert EditTextTyped to Kotlin * fixed pick media button disabled color * force sensitive media when content warning is shown * add db cache for emojis & fix tests * reorder buttons to match mastodon web * add possibility to cancel sending of toot * correctly delete sent toots * refresh SavedTootActivity after toot was sent * remove unused resources * correct params for toot saving in SendTootService * consolidate strings * bugfix * remove unused resources * fix notifications on old android for SendTootService * fix crash
This commit is contained in:
parent
8a23f034f0
commit
27eefbf65a
79 changed files with 1815 additions and 1234 deletions
|
@ -7,7 +7,6 @@ import android.support.design.widget.Snackbar;
|
|||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -29,7 +29,6 @@ import android.support.design.widget.TabLayout;
|
|||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
|
@ -43,7 +42,6 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.NotificationHelper;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||
|
@ -242,16 +240,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity,
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
|
||||
Intent intent = new Intent(TimelineReceiver.Types.STATUS_COMPOSED);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext())
|
||||
.sendBroadcast(intent);
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (drawer != null && drawer.isDrawerOpen()) {
|
||||
|
|
|
@ -15,27 +15,29 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
||||
import com.keylesspalace.tusky.db.TootDao;
|
||||
import com.keylesspalace.tusky.db.TootEntity;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
@ -43,11 +45,12 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction {
|
||||
private static final String TAG = "SavedTootActivity"; // logging tag
|
||||
|
||||
// dao
|
||||
private static TootDao tootDao = TuskyApplication.getDB().tootDao();
|
||||
|
||||
private SaveTootHelper saveTootHelper;
|
||||
|
||||
// ui
|
||||
private SavedTootAdapter adapter;
|
||||
private TextView noContent;
|
||||
|
@ -55,9 +58,27 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
|||
private List<TootEntity> toots = new ArrayList<>();
|
||||
@Nullable private AsyncTask<?, ?, ?> asyncTask;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
saveTootHelper = new SaveTootHelper(tootDao, this);
|
||||
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
fetchToots();
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(TimelineReceiver.Types.STATUS_COMPOSED);
|
||||
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.registerReceiver(broadcastReceiver, intentFilter);
|
||||
|
||||
setContentView(R.layout.activity_saved_toot);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
@ -96,6 +117,12 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
|||
if (asyncTask != null) asyncTask.cancel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
|
@ -122,19 +149,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
|||
|
||||
@Override
|
||||
public void delete(int position, TootEntity item) {
|
||||
// Delete any media files associated with the status.
|
||||
ArrayList<String> uris = new Gson().fromJson(item.getUrls(),
|
||||
new TypeToken<ArrayList<String>>() {}.getType());
|
||||
if (uris != null) {
|
||||
for (String uriString : uris) {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if (getContentResolver().delete(uri, null, null) == 0) {
|
||||
Log.e(TAG, String.format("Did not delete file %s.", uriString));
|
||||
}
|
||||
}
|
||||
}
|
||||
// update DB
|
||||
tootDao.delete(item.getUid());
|
||||
|
||||
saveTootHelper.deleteDraft(item);
|
||||
|
||||
toots.remove(position);
|
||||
// update adapter
|
||||
if (adapter != null) {
|
||||
|
|
|
@ -17,6 +17,7 @@ package com.keylesspalace.tusky;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.Service;
|
||||
import android.app.UiModeManager;
|
||||
import android.arch.persistence.room.Room;
|
||||
import android.content.Context;
|
||||
|
@ -39,10 +40,11 @@ import javax.inject.Inject;
|
|||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.HasActivityInjector;
|
||||
import dagger.android.HasServiceInjector;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class TuskyApplication extends Application implements HasActivityInjector {
|
||||
public class TuskyApplication extends Application implements HasActivityInjector, HasServiceInjector {
|
||||
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
|
||||
|
||||
private static AppDatabase db;
|
||||
|
@ -50,6 +52,8 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
@Inject
|
||||
DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;
|
||||
@Inject
|
||||
DispatchingAndroidInjector<Service> dispatchingServiceInjector;
|
||||
@Inject
|
||||
NotificationPullJobCreator notificationPullJobCreator;
|
||||
|
||||
public static AppDatabase getDB() {
|
||||
|
@ -75,7 +79,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
|
||||
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
|
||||
.allowMainThreadQueries()
|
||||
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5)
|
||||
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6)
|
||||
.build();
|
||||
accountManager = new AccountManager(db);
|
||||
serviceLocator = new ServiceLocator() {
|
||||
|
@ -90,7 +94,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
}
|
||||
};
|
||||
|
||||
AppInjector.INSTANCE.init(this);
|
||||
initAppInjector();
|
||||
initPicasso();
|
||||
|
||||
JobManager.create(this).addJobCreator(notificationPullJobCreator);
|
||||
|
@ -100,6 +104,10 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
protected void initAppInjector() {
|
||||
AppInjector.INSTANCE.init(this);
|
||||
}
|
||||
|
||||
protected void initPicasso() {
|
||||
// Initialize Picasso configuration
|
||||
Picasso.Builder builder = new Picasso.Builder(this);
|
||||
|
@ -128,6 +136,11 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
return dispatchingAndroidInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Service> serviceInjector() {
|
||||
return dispatchingServiceInjector;
|
||||
}
|
||||
|
||||
public interface ServiceLocator {
|
||||
<T> T get(Class<T> clazz);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.squareup.picasso.Picasso
|
||||
|
||||
class EmojiAdapter(private val emojiList: List<Emoji>, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter<EmojiAdapter.EmojiHolder>() {
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return emojiList.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiAdapter.EmojiHolder {
|
||||
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji_button, parent, false) as ImageView
|
||||
return EmojiHolder(view)
|
||||
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: EmojiAdapter.EmojiHolder, position: Int) {
|
||||
Picasso.with(viewHolder.emojiImageView.context)
|
||||
.load(emojiList[position].url)
|
||||
.into(viewHolder.emojiImageView)
|
||||
|
||||
viewHolder.emojiImageView.setOnClickListener {
|
||||
onEmojiSelectedListener.onEmojiSelected(emojiList[position].shortcode)
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView)
|
||||
|
||||
}
|
||||
|
||||
interface OnEmojiSelectedListener {
|
||||
fun onEmojiSelected(shortcode: String)
|
||||
}
|
|
@ -37,8 +37,8 @@ import android.widget.TextView;
|
|||
import android.widget.ToggleButton;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
|
@ -485,7 +485,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
|
||||
Spanned content = statusViewData.getContent();
|
||||
List<Status.Emoji> emojis = statusViewData.getEmojis();
|
||||
List<Emoji> emojis = statusViewData.getEmojis();
|
||||
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent);
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.widget.ToggleButton;
|
|||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
|
@ -106,7 +107,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
username.setText(usernameText);
|
||||
}
|
||||
|
||||
private void setContent(Spanned content, Status.Mention[] mentions, List<Status.Emoji> emojis,
|
||||
private void setContent(Spanned content, Status.Mention[] mentions, List<Emoji> emojis,
|
||||
StatusActionListener listener) {
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
||||
|
||||
|
@ -384,7 +385,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
sensitiveMediaShow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setSpoilerText(String spoilerText, List<Status.Emoji> emojis,
|
||||
private void setSpoilerText(String spoilerText, List<Emoji> emojis,
|
||||
final boolean expanded, final StatusActionListener listener) {
|
||||
CharSequence emojiSpoiler =
|
||||
CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
||||
|
|
|
@ -15,9 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.db
|
||||
|
||||
import android.arch.persistence.room.Database
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,11 +25,12 @@ import android.support.annotation.NonNull;
|
|||
* DB version & declare DAO
|
||||
*/
|
||||
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class}, version = 5, exportSchema = false)
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class, EmojiListEntity.class}, version = 6, exportSchema = false)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract TootDao tootDao();
|
||||
public abstract AccountDao accountDao();
|
||||
public abstract EmojiListDao emojiListDao();
|
||||
|
||||
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||
@Override
|
||||
|
@ -74,4 +75,11 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))");
|
||||
}
|
||||
};
|
||||
}
|
31
app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt
Normal file
31
app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.db
|
||||
|
||||
import android.arch.persistence.room.Dao
|
||||
import android.arch.persistence.room.Insert
|
||||
import android.arch.persistence.room.OnConflictStrategy
|
||||
import android.arch.persistence.room.Query
|
||||
|
||||
@Dao
|
||||
interface EmojiListDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(emojiList: EmojiListEntity)
|
||||
|
||||
@Query("SELECT * FROM EmojiListEntity WHERE instance = :instance LIMIT 1")
|
||||
fun loadEmojisForInstance(instance: String): EmojiListEntity?
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.db
|
||||
|
||||
import android.arch.persistence.room.Entity
|
||||
import android.arch.persistence.room.PrimaryKey
|
||||
import android.arch.persistence.room.TypeConverter
|
||||
import android.arch.persistence.room.TypeConverters
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
|
||||
@Entity
|
||||
@TypeConverters(Converters::class)
|
||||
data class EmojiListEntity(@field:PrimaryKey var instance: String,
|
||||
val emojiList: List<Emoji>)
|
||||
|
||||
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToList(emojiListJson: String): List<Emoji> {
|
||||
return Gson().fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun listToJson(emojiList: List<Emoji>): String {
|
||||
return Gson().toJson(emojiList)
|
||||
}
|
||||
}
|
|
@ -38,4 +38,7 @@ public interface TootDao {
|
|||
|
||||
@Query("DELETE FROM TootEntity WHERE uid = :uid")
|
||||
int delete(int uid);
|
||||
|
||||
@Query("SELECT * FROM TootEntity WHERE uid = :uid")
|
||||
TootEntity find(int uid);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@ import javax.inject.Singleton
|
|||
AppModule::class,
|
||||
NetworkModule::class,
|
||||
AndroidInjectionModule::class,
|
||||
ActivitiesModule::class
|
||||
ActivitiesModule::class,
|
||||
ServicesModule::class
|
||||
])
|
||||
interface AppComponent {
|
||||
@Component.Builder
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.di
|
||||
|
||||
import com.keylesspalace.tusky.service.SendTootService
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@Module
|
||||
abstract class ServicesModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeMyService(): SendTootService
|
||||
}
|
25
app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
Normal file
25
app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val url: String
|
||||
) : Parcelable
|
|
@ -131,11 +131,6 @@ data class Status(
|
|||
var website: String? = null
|
||||
}
|
||||
|
||||
class Emoji {
|
||||
val shortcode: String? = null
|
||||
val url: String? = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_MEDIA_ATTACHMENTS = 4
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import com.keylesspalace.tusky.AccountActivity;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.adapter.AccountAdapter;
|
||||
import com.keylesspalace.tusky.adapter.BlocksAdapter;
|
||||
|
|
|
@ -1,161 +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>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.BottomSheetDialogFragment;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.graphics.drawable.DrawableCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
|
||||
public class ComposeOptionsFragment extends BottomSheetDialogFragment {
|
||||
public interface Listener {
|
||||
void onVisibilityChanged(Status.Visibility visibility);
|
||||
void onContentWarningChanged(boolean hideText);
|
||||
}
|
||||
|
||||
private RadioGroup radio;
|
||||
private CheckBox hideText;
|
||||
private Listener listener;
|
||||
|
||||
public static ComposeOptionsFragment newInstance(Status.Visibility visibility, boolean hideText) {
|
||||
Bundle arguments = new Bundle();
|
||||
ComposeOptionsFragment fragment = new ComposeOptionsFragment();
|
||||
arguments.putInt("visibilityNum", visibility.getNum());
|
||||
arguments.putBoolean("hideText", hideText);
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
listener = (Listener) context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false);
|
||||
|
||||
Bundle arguments = getArguments();
|
||||
Status.Visibility visibility = Status.Visibility.byNum(
|
||||
arguments.getInt("visibilityNum", 0)
|
||||
);
|
||||
boolean statusHideText = arguments.getBoolean("hideText");
|
||||
|
||||
radio = rootView.findViewById(R.id.radio_visibility);
|
||||
int radioCheckedId = R.id.radio_public;
|
||||
switch (visibility) {
|
||||
case PUBLIC: radioCheckedId = R.id.radio_public; break;
|
||||
case PRIVATE: radioCheckedId = R.id.radio_private; break;
|
||||
case UNLISTED: radioCheckedId = R.id.radio_unlisted; break;
|
||||
case DIRECT: radioCheckedId = R.id.radio_direct; break;
|
||||
}
|
||||
radio.check(radioCheckedId);
|
||||
|
||||
RadioButton publicButton = rootView.findViewById(R.id.radio_public);
|
||||
RadioButton unlistedButton = rootView.findViewById(R.id.radio_unlisted);
|
||||
RadioButton privateButton = rootView.findViewById(R.id.radio_private);
|
||||
RadioButton directButton = rootView.findViewById(R.id.radio_direct);
|
||||
setRadioButtonDrawable(getContext(), publicButton, R.drawable.ic_public_24dp);
|
||||
setRadioButtonDrawable(getContext(), unlistedButton, R.drawable.ic_lock_open_24dp);
|
||||
setRadioButtonDrawable(getContext(), privateButton, R.drawable.ic_lock_outline_24dp);
|
||||
setRadioButtonDrawable(getContext(), directButton, R.drawable.ic_email_24dp);
|
||||
|
||||
hideText = rootView.findViewById(R.id.compose_hide_text);
|
||||
hideText.setChecked(statusHideText);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||
Status.Visibility visibility;
|
||||
switch (checkedId) {
|
||||
default:
|
||||
case R.id.radio_public: {
|
||||
visibility = Status.Visibility.PUBLIC;
|
||||
break;
|
||||
}
|
||||
case R.id.radio_unlisted: {
|
||||
visibility = Status.Visibility.UNLISTED;
|
||||
break;
|
||||
}
|
||||
case R.id.radio_private: {
|
||||
visibility = Status.Visibility.PRIVATE;
|
||||
break;
|
||||
}
|
||||
case R.id.radio_direct: {
|
||||
visibility = Status.Visibility.DIRECT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
listener.onVisibilityChanged(visibility);
|
||||
}
|
||||
});
|
||||
hideText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
listener.onContentWarningChanged(isChecked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void setRadioButtonDrawable(Context context, RadioButton button,
|
||||
@DrawableRes int id) {
|
||||
ColorStateList list = new ColorStateList(new int[][] {
|
||||
new int[] { -android.R.attr.state_checked },
|
||||
new int[] { android.R.attr.state_checked }
|
||||
}, new int[] {
|
||||
ThemeUtils.getColor(context, R.attr.compose_image_button_tint),
|
||||
ThemeUtils.getColor(context, R.attr.colorAccent)
|
||||
});
|
||||
Drawable drawable = VectorDrawableCompat.create(context.getResources(), id,
|
||||
context.getTheme());
|
||||
if (drawable == null) {
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
button.setButtonTintList(list);
|
||||
} else {
|
||||
drawable = DrawableCompat.wrap(drawable);
|
||||
DrawableCompat.setTintList(drawable, list);
|
||||
}
|
||||
button.setButtonDrawable(drawable);
|
||||
}
|
||||
}
|
|
@ -20,18 +20,14 @@ import android.content.ClipboardManager;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.text.Spanned;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.keylesspalace.tusky.AccountActivity;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.ComposeActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ReportActivity;
|
||||
|
@ -41,27 +37,16 @@ import com.keylesspalace.tusky.ViewTagActivity;
|
|||
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||
import com.keylesspalace.tusky.ViewVideoActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.network.TimelineCases;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
|
||||
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
|
||||
* of that is complicated by how they're coupled with Status and Notification and the corresponding
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.keylesspalace.tusky.entity.AppCredentials;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.MastoList;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
|
@ -101,12 +102,15 @@ public interface MastodonApi {
|
|||
@FormUrlEncoded
|
||||
@POST("api/v1/statuses")
|
||||
Call<Status> createStatus(
|
||||
@Header("Authorization") String auth,
|
||||
@Header(DOMAIN_HEADER) String domain,
|
||||
@Field("status") String text,
|
||||
@Field("in_reply_to_id") String inReplyToId,
|
||||
@Field("spoiler_text") String warningText,
|
||||
@Field("visibility") String visibility,
|
||||
@Field("sensitive") Boolean sensitive,
|
||||
@Field("media_ids[]") List<String> mediaIds);
|
||||
@Field("media_ids[]") List<String> mediaIds,
|
||||
@Header("Idempotency-Key") String idempotencyKey);
|
||||
@GET("api/v1/statuses/{id}")
|
||||
Call<Status> status(@Path("id") String statusId);
|
||||
@GET("api/v1/statuses/{id}/context")
|
||||
|
@ -263,4 +267,7 @@ public interface MastodonApi {
|
|||
|
||||
@GET("/api/v1/lists")
|
||||
Call<List<MastoList>> getLists();
|
||||
|
||||
@GET("/api/v1/custom_emojis")
|
||||
Call<List<Emoji>> getCustomEmojis();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
package com.keylesspalace.tusky.service
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.ServiceCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver
|
||||
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||
import com.keylesspalace.tusky.util.StringUtils
|
||||
import dagger.android.AndroidInjection
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendTootService: Service(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
private lateinit var saveTootHelper: SaveTootHelper
|
||||
|
||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||
|
||||
private val timer = Timer()
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
AndroidInjection.inject(this)
|
||||
saveTootHelper = SaveTootHelper(TuskyApplication.getDB().tootDao(), this)
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
|
||||
if(intent.hasExtra(KEY_TOOT)) {
|
||||
|
||||
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
||||
|
||||
if (tootToSend == null) {
|
||||
throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
|
||||
}
|
||||
|
||||
var notificationText = tootToSend.warningText
|
||||
if (notificationText.isBlank()) {
|
||||
notificationText = tootToSend.text
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(getString(R.string.send_toot_notification_title))
|
||||
.setContentText(notificationText)
|
||||
.setProgress(1, 0, true)
|
||||
.setOngoing(true)
|
||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(notificationId))
|
||||
|
||||
if(tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
startForeground(notificationId, builder.build())
|
||||
} else {
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
tootsToSend[notificationId] = tootToSend
|
||||
sendToot(notificationId)
|
||||
|
||||
notificationId--
|
||||
|
||||
} else {
|
||||
|
||||
if(intent.hasExtra(KEY_CANCEL)) {
|
||||
cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
|
||||
stopSelf(intent.getIntExtra(KEY_CANCEL, 0))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
|
||||
}
|
||||
|
||||
private fun sendToot(tootId: Int) {
|
||||
|
||||
// when tootToSend == null, sending has been canceled
|
||||
val tootToSend = tootsToSend[tootId] ?: return
|
||||
|
||||
// when account == null, user has logged out, cancel sending
|
||||
val account = accountManager.getAccountById(tootToSend.accountId)
|
||||
|
||||
if(account == null) {
|
||||
tootsToSend.remove(tootId)
|
||||
return
|
||||
}
|
||||
|
||||
tootToSend.retries++
|
||||
|
||||
val sendCall = mastodonApi.createStatus(
|
||||
"Bearer " + account.accessToken,
|
||||
account.domain,
|
||||
tootToSend.text,
|
||||
tootToSend.inReplyToId,
|
||||
tootToSend.warningText,
|
||||
tootToSend.visibility,
|
||||
tootToSend.sensitive,
|
||||
tootToSend.mediaIds,
|
||||
tootToSend.idempotencyKey
|
||||
)
|
||||
|
||||
|
||||
sendCalls[tootId] = sendCall
|
||||
|
||||
val callback = object: Callback<Status> {
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
|
||||
tootsToSend.remove(tootId)
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (response.isSuccessful) {
|
||||
|
||||
val intent = Intent(TimelineReceiver.Types.STATUS_COMPOSED)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
|
||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||
if(tootToSend.savedTootUid != 0) {
|
||||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||
}
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
notificationManager.cancel(tootId)
|
||||
|
||||
} else {
|
||||
// the server refused to accept the toot, save toot & show error message
|
||||
saveTootToDrafts(tootToSend)
|
||||
|
||||
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(getString(R.string.send_toot_notification_error_title))
|
||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
|
||||
|
||||
notificationManager.notify(tootId, builder.build())
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
var backoff = 1000L*tootToSend.retries
|
||||
if (backoff > MAX_RETRY_INTERVAL) {
|
||||
backoff = MAX_RETRY_INTERVAL
|
||||
}
|
||||
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
sendToot(tootId)
|
||||
}
|
||||
}, backoff)
|
||||
}
|
||||
}
|
||||
|
||||
sendCall.enqueue(callback)
|
||||
|
||||
}
|
||||
|
||||
private fun cancelSending(tootId: Int) {
|
||||
val tootToCancel = tootsToSend.remove(tootId)
|
||||
if(tootToCancel != null) {
|
||||
val sendCall = sendCalls.remove(tootId)
|
||||
sendCall?.cancel()
|
||||
|
||||
saveTootToDrafts(tootToCancel)
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(getString(R.string.send_toot_notification_cancel_title))
|
||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
|
||||
|
||||
notificationManager.notify(tootId, builder.build())
|
||||
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
notificationManager.cancel(tootId)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTootToDrafts(toot: TootToSend) {
|
||||
|
||||
saveTootHelper.saveToot(toot.text,
|
||||
toot.warningText,
|
||||
toot.savedJsonUrls,
|
||||
toot.mediaUris,
|
||||
toot.savedTootUid,
|
||||
toot.inReplyToId,
|
||||
toot.replyingStatusContent,
|
||||
toot.replyingStatusAuthorUsername,
|
||||
Status.Visibility.byString(toot.visibility))
|
||||
}
|
||||
|
||||
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
||||
|
||||
val intent = Intent(this, SendTootService::class.java)
|
||||
|
||||
intent.putExtra(KEY_CANCEL, tootId)
|
||||
|
||||
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_TOOT = "toot"
|
||||
private const val KEY_CANCEL = "cancel_id"
|
||||
private const val CHANNEL_ID = "send_toots"
|
||||
|
||||
private const val MAX_RETRY_INTERVAL = 60*1000L // 1 minute
|
||||
|
||||
private var notificationId = -1 // use negative ids to not clash with other notis
|
||||
|
||||
@JvmStatic
|
||||
fun sendTootIntent(context: Context,
|
||||
text: String,
|
||||
warningText: String,
|
||||
visibility: Status.Visibility,
|
||||
sensitive: Boolean,
|
||||
mediaIds: List<String>,
|
||||
mediaUris: List<String>,
|
||||
inReplyToId: String?,
|
||||
replyingStatusContent: String?,
|
||||
replyingStatusAuthorUsername: String?,
|
||||
savedJsonUrls: String?,
|
||||
account: AccountEntity,
|
||||
savedTootUid: Int
|
||||
): Intent {
|
||||
val intent = Intent(context, SendTootService::class.java)
|
||||
|
||||
val idempotencyKey = StringUtils.randomAlphanumericString(16)
|
||||
|
||||
val tootToSend = TootToSend(text,
|
||||
warningText,
|
||||
visibility.serverString(),
|
||||
sensitive,
|
||||
mediaIds,
|
||||
mediaUris,
|
||||
inReplyToId,
|
||||
replyingStatusContent,
|
||||
replyingStatusAuthorUsername,
|
||||
savedJsonUrls,
|
||||
account.id,
|
||||
savedTootUid,
|
||||
idempotencyKey,
|
||||
0)
|
||||
|
||||
intent.putExtra(KEY_TOOT, tootToSend)
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class TootToSend(val text: String,
|
||||
val warningText: String,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
val mediaIds: List<String>,
|
||||
val mediaUris: List<String>,
|
||||
val inReplyToId: String?,
|
||||
val replyingStatusContent: String?,
|
||||
val replyingStatusAuthorUsername: String?,
|
||||
val savedJsonUrls: String?,
|
||||
val accountId: Long,
|
||||
val savedTootUid: Int,
|
||||
val idempotencyKey: String,
|
||||
var retries: Int): Parcelable
|
|
@ -28,7 +28,7 @@ import android.text.SpannedString;
|
|||
import android.text.style.ReplacementSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
|
@ -46,12 +46,12 @@ public class CustomEmojiHelper {
|
|||
* @param textView a reference to the textView the emojis will be shown in
|
||||
* @return the text with the shortcodes replaced by EmojiSpans
|
||||
*/
|
||||
public static Spanned emojifyText(Spanned text, List<Status.Emoji> emojis, final TextView textView) {
|
||||
public static Spanned emojifyText(Spanned text, List<Emoji> emojis, final TextView textView) {
|
||||
|
||||
if (!emojis.isEmpty()) {
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
for (Status.Emoji emoji : emojis) {
|
||||
for (Emoji emoji : emojis) {
|
||||
CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':');
|
||||
Matcher matcher = Pattern.compile(pattern.toString()).matcher(text);
|
||||
while (matcher.find()) {
|
||||
|
@ -71,7 +71,7 @@ public class CustomEmojiHelper {
|
|||
return text;
|
||||
}
|
||||
|
||||
public static Spanned emojifyString(String string, List<Status.Emoji> emojis, final TextView textView) {
|
||||
public static Spanned emojifyString(String string, List<Emoji> emojis, final TextView textView) {
|
||||
return emojifyText(new SpannedString(string), emojis, textView);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,14 +15,22 @@
|
|||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class IOUtils {
|
||||
public static void closeQuietly(@Nullable InputStream stream) {
|
||||
|
||||
private static final int DEFAULT_BLOCKSIZE = 16384;
|
||||
|
||||
public static void closeQuietly(@Nullable Closeable stream) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
|
@ -32,13 +40,32 @@ public class IOUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void closeQuietly(@Nullable OutputStream stream) {
|
||||
public static boolean copyToFile(ContentResolver contentResolver, Uri uri, File file) {
|
||||
InputStream from;
|
||||
FileOutputStream to;
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
from = contentResolver.openInputStream(uri);
|
||||
to = new FileOutputStream(file);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
if (from == null) {
|
||||
return false;
|
||||
}
|
||||
byte[] chunk = new byte[DEFAULT_BLOCKSIZE];
|
||||
try {
|
||||
while (true) {
|
||||
int bytes = from.read(chunk, 0, chunk.length);
|
||||
if (bytes < 0) {
|
||||
break;
|
||||
}
|
||||
to.write(chunk, 0, bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// intentionally unhandled
|
||||
return false;
|
||||
}
|
||||
closeQuietly(from);
|
||||
closeQuietly(to);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
import com.keylesspalace.tusky.db.TootDao;
|
||||
import com.keylesspalace.tusky.db.TootEntity;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class SaveTootHelper {
|
||||
|
||||
private static final String TAG = "SaveTootHelper";
|
||||
|
||||
private TootDao tootDao;
|
||||
private Context context;
|
||||
|
||||
public SaveTootHelper(@NonNull TootDao tootDao, @NonNull Context context) {
|
||||
this.tootDao = tootDao;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public boolean saveToot(@NonNull String content,
|
||||
@NonNull String contentWarning,
|
||||
@Nullable String savedJsonUrls,
|
||||
@NonNull List<String> mediaUris,
|
||||
int savedTootUid,
|
||||
@Nullable String inReplyToId,
|
||||
@Nullable String replyingStatusContent,
|
||||
@Nullable String replyingStatusAuthorUsername,
|
||||
@NonNull Status.Visibility statusVisibility) {
|
||||
|
||||
if (TextUtils.isEmpty(content) && mediaUris.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get any existing file's URIs.
|
||||
ArrayList<String> existingUris = null;
|
||||
if (!TextUtils.isEmpty(savedJsonUrls)) {
|
||||
existingUris = new Gson().fromJson(savedJsonUrls,
|
||||
new TypeToken<ArrayList<String>>() {
|
||||
}.getType());
|
||||
}
|
||||
|
||||
String mediaUrlsSerialized = null;
|
||||
if (!ListUtils.isEmpty(mediaUris)) {
|
||||
List<String> savedList = saveMedia(mediaUris, existingUris);
|
||||
if (!ListUtils.isEmpty(savedList)) {
|
||||
mediaUrlsSerialized = new Gson().toJson(savedList);
|
||||
if (!ListUtils.isEmpty(existingUris)) {
|
||||
deleteMedia(setDifference(existingUris, savedList));
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (!ListUtils.isEmpty(existingUris)) {
|
||||
/* If there were URIs in the previous draft, but they've now been removed, those files
|
||||
* can be deleted. */
|
||||
deleteMedia(existingUris);
|
||||
}
|
||||
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, contentWarning,
|
||||
inReplyToId,
|
||||
replyingStatusContent,
|
||||
replyingStatusAuthorUsername,
|
||||
statusVisibility);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
tootDao.insertOrReplace(toot);
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void deleteDraft(int tootId) {
|
||||
TootEntity item = tootDao.find(tootId);
|
||||
if(item != null) {
|
||||
deleteDraft(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteDraft(@NonNull TootEntity item){
|
||||
// Delete any media files associated with the status.
|
||||
ArrayList<String> uris = new Gson().fromJson(item.getUrls(),
|
||||
new TypeToken<ArrayList<String>>() {}.getType());
|
||||
if (uris != null) {
|
||||
for (String uriString : uris) {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if (context.getContentResolver().delete(uri, null, null) == 0) {
|
||||
Log.e(TAG, String.format("Did not delete file %s.", uriString));
|
||||
}
|
||||
}
|
||||
}
|
||||
// update DB
|
||||
tootDao.delete(item.getUid());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private List<String> saveMedia(@NonNull List<String> mediaUris,
|
||||
@Nullable List<String> existingUris) {
|
||||
|
||||
File directory = context.getExternalFilesDir("Tusky");
|
||||
|
||||
if (directory == null || !(directory.exists())) {
|
||||
Log.e(TAG, "Error obtaining directory to save media.");
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
ArrayList<File> filesSoFar = new ArrayList<>();
|
||||
ArrayList<String> results = new ArrayList<>();
|
||||
for (String mediaUri : mediaUris) {
|
||||
/* If the media was already saved in a previous draft, there's no need to save another
|
||||
* copy, just add the existing URI to the results. */
|
||||
if (existingUris != null) {
|
||||
int index = existingUris.indexOf(mediaUri);
|
||||
if (index != -1) {
|
||||
results.add(mediaUri);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Otherwise, save the media.
|
||||
|
||||
Uri uri = Uri.parse(mediaUri);
|
||||
|
||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
|
||||
|
||||
String mimeType = contentResolver.getType(uri);
|
||||
MimeTypeMap map = MimeTypeMap.getSingleton();
|
||||
String fileExtension = map.getExtensionFromMimeType(mimeType);
|
||||
String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension);
|
||||
File file = new File(directory, filename);
|
||||
filesSoFar.add(file);
|
||||
boolean copied = IOUtils.copyToFile(contentResolver, uri, file);
|
||||
if (!copied) {
|
||||
/* If any media files were created in prior iterations, delete those before
|
||||
* returning. */
|
||||
for (File earlierFile : filesSoFar) {
|
||||
boolean deleted = earlierFile.delete();
|
||||
if (!deleted) {
|
||||
Log.i(TAG, "Could not delete the file " + earlierFile.toString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID+".fileprovider", file);
|
||||
results.add(resultUri.toString());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private void deleteMedia(List<String> mediaUris) {
|
||||
for (String uriString : mediaUris) {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if (context.getContentResolver().delete(uri, null, null) == 0) {
|
||||
Log.e(TAG, String.format("Did not delete file %s.", uriString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A∖B={x∈A|x∉B}
|
||||
*
|
||||
* @return all elements of set A that are not in set B.
|
||||
*/
|
||||
private static List<String> setDifference(List<String> a, List<String> b) {
|
||||
List<String> c = new ArrayList<>();
|
||||
for (String s : a) {
|
||||
if (!b.contains(s)) {
|
||||
c.add(s);
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import kotlinx.android.synthetic.main.view_compose_options.view.*
|
||||
|
||||
|
||||
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
var listener: ComposeOptionsListener? = null
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_compose_options, this)
|
||||
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
publicRadioButton.setButtonDrawable(R.drawable.ic_public_24dp)
|
||||
unlistedRadioButton.setButtonDrawable(R.drawable.ic_lock_open_24dp)
|
||||
privateRadioButton.setButtonDrawable(R.drawable.ic_lock_outline_24dp)
|
||||
directRadioButton.setButtonDrawable(R.drawable.ic_email_24dp)
|
||||
}
|
||||
|
||||
visibilityRadioGroup.setOnCheckedChangeListener({ _, checkedId ->
|
||||
val visibility = when (checkedId) {
|
||||
R.id.publicRadioButton ->
|
||||
Status.Visibility.PUBLIC
|
||||
R.id.unlistedRadioButton ->
|
||||
Status.Visibility.UNLISTED
|
||||
R.id.privateRadioButton ->
|
||||
Status.Visibility.PRIVATE
|
||||
R.id.directRadioButton ->
|
||||
Status.Visibility.DIRECT
|
||||
else ->
|
||||
Status.Visibility.PUBLIC
|
||||
}
|
||||
listener?.onVisibilityChanged(visibility)
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatusVisibility(visibility: Status.Visibility) {
|
||||
val selectedButton = when (visibility) {
|
||||
Status.Visibility.PUBLIC ->
|
||||
R.id.publicRadioButton
|
||||
Status.Visibility.UNLISTED ->
|
||||
R.id.unlistedRadioButton
|
||||
Status.Visibility.PRIVATE ->
|
||||
R.id.privateRadioButton
|
||||
Status.Visibility.DIRECT ->
|
||||
R.id.directRadioButton
|
||||
else ->
|
||||
R.id.directRadioButton
|
||||
|
||||
}
|
||||
|
||||
visibilityRadioGroup.check(selectedButton)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ComposeOptionsListener {
|
||||
fun onVisibilityChanged(visibility: Status.Visibility)
|
||||
}
|
|
@ -1,59 +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>. */
|
||||
|
||||
package com.keylesspalace.tusky.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
||||
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.keylesspalace.tusky.util.Assert;
|
||||
|
||||
public class EditTextTyped extends AppCompatMultiAutoCompleteTextView {
|
||||
|
||||
private InputConnectionCompat.OnCommitContentListener onCommitContentListener;
|
||||
private String[] mimeTypes;
|
||||
|
||||
public EditTextTyped(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EditTextTyped(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
public void setMimeTypes(String[] types,
|
||||
InputConnectionCompat.OnCommitContentListener listener) {
|
||||
mimeTypes = types;
|
||||
onCommitContentListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
InputConnection connection = super.onCreateInputConnection(editorInfo);
|
||||
if (onCommitContentListener != null) {
|
||||
Assert.expect(mimeTypes != null);
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
|
||||
return InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener);
|
||||
} else {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.view
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat
|
||||
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
|
||||
class EditTextTyped @JvmOverloads constructor(context: Context,
|
||||
attributeSet: AttributeSet? = null)
|
||||
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
|
||||
init {
|
||||
//fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
}
|
||||
|
||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
||||
onCommitContentListener = listener
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener!!)
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
}
|
||||
}
|
76
app/src/main/java/com/keylesspalace/tusky/view/TootButton.kt
Normal file
76
app/src/main/java/com/keylesspalace/tusky/view/TootButton.kt
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.support.v7.widget.AppCompatButton
|
||||
import android.util.AttributeSet
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
|
||||
class TootButton
|
||||
@JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : AppCompatButton(context, attrs, defStyleAttr) {
|
||||
|
||||
private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
|
||||
|
||||
init {
|
||||
if(smallStyle) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_send_24dp, 0, 0, 0)
|
||||
} else {
|
||||
compoundDrawablePadding = context.resources.getDimensionPixelSize(R.dimen.toot_button_drawable_padding)
|
||||
setText(R.string.action_send)
|
||||
}
|
||||
}
|
||||
|
||||
fun setStatusVisibility(visibility: Status.Visibility) {
|
||||
if(!smallStyle) {
|
||||
|
||||
when (visibility) {
|
||||
Status.Visibility.PUBLIC -> {
|
||||
setText(R.string.action_send_public)
|
||||
setCompoundDrawables(null, null, null, null)
|
||||
}
|
||||
Status.Visibility.UNLISTED -> {
|
||||
setText(R.string.action_send)
|
||||
setCompoundDrawables(null, null, null, null)
|
||||
}
|
||||
Status.Visibility.PRIVATE,
|
||||
Status.Visibility.DIRECT -> {
|
||||
addLock()
|
||||
}
|
||||
else -> {
|
||||
setCompoundDrawables(null, null, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun addLock() {
|
||||
setText(R.string.action_send)
|
||||
val lock = IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).sizeDp(18).color(Color.WHITE)
|
||||
setCompoundDrawablesWithIntrinsicBounds(lock, null, null, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import android.text.Spanned;
|
|||
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -68,7 +69,7 @@ public abstract class StatusViewData {
|
|||
private final String senderId;
|
||||
private final boolean rebloggingEnabled;
|
||||
private final Status.Application application;
|
||||
private final List<Status.Emoji> emojis;
|
||||
private final List<Emoji> emojis;
|
||||
@Nullable
|
||||
private final Card card;
|
||||
|
||||
|
@ -78,7 +79,7 @@ public abstract class StatusViewData {
|
|||
boolean isShowingContent, String userFullName, String nickname, String avatar,
|
||||
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
|
||||
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||
Status.Application application, List<Status.Emoji> emojis, @Nullable Card card) {
|
||||
Status.Application application, List<Emoji> emojis, @Nullable Card card) {
|
||||
this.id = id;
|
||||
this.content = content;
|
||||
this.reblogged = reblogged;
|
||||
|
@ -203,7 +204,7 @@ public abstract class StatusViewData {
|
|||
return application;
|
||||
}
|
||||
|
||||
public List<Status.Emoji> getEmojis() {
|
||||
public List<Emoji> getEmojis() {
|
||||
return emojis;
|
||||
}
|
||||
|
||||
|
@ -250,7 +251,7 @@ public abstract class StatusViewData {
|
|||
private String senderId;
|
||||
private boolean rebloggingEnabled;
|
||||
private Status.Application application;
|
||||
private List<Status.Emoji> emojis;
|
||||
private List<Emoji> emojis;
|
||||
private Card card;
|
||||
|
||||
public Builder() {
|
||||
|
@ -399,7 +400,7 @@ public abstract class StatusViewData {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setEmojis(List<Status.Emoji> emojis) {
|
||||
public Builder setEmojis(List<Emoji> emojis) {
|
||||
this.emojis = emojis;
|
||||
return this;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue