Refactor quick compose

* Stash visibility while composing reply
* Remove announcements
This commit is contained in:
kyori19 2020-09-07 22:57:59 +09:00
parent bb5106a18d
commit 025ba68df4
23 changed files with 326 additions and 563 deletions

View File

@ -34,10 +34,10 @@ import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.ImageView
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
@ -65,6 +65,7 @@ import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.fragment.SFragment
@ -94,7 +95,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import net.accelf.yuito.FooterDrawerItem
import net.accelf.yuito.QuickTootHelper
import net.accelf.yuito.QuickTootViewModel
import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
@ -110,6 +111,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject
lateinit var conversationRepository: ConversationsRepository
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
private lateinit var header: AccountHeaderView
private var streamingTabsCount = 0
@ -182,10 +188,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(R.layout.activity_main)
val quickTootHelper = QuickTootHelper(this, quickTootContainer as ConstraintLayout?, accountManager, eventHub)
composeButton.setOnClickListener {
quickTootHelper.composeButton()
}
viewQuickToot.attachViewModel(quickTootViewModel, this)
composeButton.setOnClickListener(viewQuickToot::onFABClicked)
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
mainToolbar.visible(!hideTopToolbar)
@ -238,7 +242,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
is MainTabsChangedEvent -> setupTabs(false)
}
quickTootHelper.handleEvent(event)
viewQuickToot.handleEvent(event)
}
// Flush old media that was cached for sharing
@ -475,7 +479,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
)
addStickyDrawerItems(
FooterDrawerItem().apply {
eventHub = this@MainActivity.eventHub
setSubscribeProxy(
mastodonApi.getInstance()
.observeOn(AndroidSchedulers.mainThread())

View File

@ -4,10 +4,11 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.uber.autodispose.AutoDispose.autoDisposable
@ -15,8 +16,10 @@ import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_main.viewQuickToot
import kotlinx.android.synthetic.main.activity_modal_timeline.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import net.accelf.yuito.QuickTootHelper
import net.accelf.yuito.QuickTootViewModel
import javax.inject.Inject
class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
@ -40,6 +43,10 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -62,15 +69,13 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
.commit()
}
val quickTootContainer = findViewById<ConstraintLayout>(R.id.quick_toot_container)
val composeButton = findViewById<FloatingActionButton>(R.id.floating_btn)
val quickTootHelper = QuickTootHelper(this, quickTootContainer, accountManager, eventHub)
viewQuickToot.attachViewModel(quickTootViewModel, this)
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.`as`(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(quickTootHelper::handleEvent)
composeButton.setOnClickListener { quickTootHelper.composeButton() }
.subscribe(viewQuickToot::handleEvent)
floating_btn.setOnClickListener(viewQuickToot::onFABClicked)
}
override fun getActionButton(): FloatingActionButton? = null

View File

@ -19,11 +19,11 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.activity.viewModels
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.fragment.TimelineFragment.Kind
import com.uber.autodispose.AutoDispose
@ -33,8 +33,9 @@ import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.extensions.CacheImplementation
import kotlinx.android.extensions.ContainerOptions
import kotlinx.android.synthetic.main.activity_statuslist.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import net.accelf.yuito.QuickTootHelper
import net.accelf.yuito.QuickTootViewModel
import javax.inject.Inject
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -43,6 +44,10 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val quickTootViewModel: QuickTootViewModel by viewModels{ viewModelFactory }
private val kind: Kind
get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
@ -71,16 +76,13 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
replace(R.id.fragment_container, fragment)
}
val quickTootContainer = findViewById<ConstraintLayout>(R.id.quick_toot_container)
val composeButton = findViewById<FloatingActionButton>(R.id.floating_btn)
val quickTootHelper = QuickTootHelper(this, quickTootContainer, accountManager, eventHub)
viewQuickToot.attachViewModel(quickTootViewModel, this)
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.`as`(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(quickTootHelper::handleEvent)
composeButton.setOnClickListener { quickTootHelper.composeButton() }
.subscribe(viewQuickToot::handleEvent)
floating_btn.setOnClickListener(viewQuickToot::onFABClicked)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@ -23,15 +23,16 @@ import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.di.ViewModelFactory;
import com.keylesspalace.tusky.fragment.TimelineFragment;
import net.accelf.yuito.QuickTootHelper;
import net.accelf.yuito.QuickTootView;
import net.accelf.yuito.QuickTootViewModel;
import java.util.Collections;
@ -53,6 +54,8 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn
public DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
@Inject
public EventHub eventHub;
@Inject
public ViewModelFactory viewModelFactory;
public static Intent getIntent(Context context, String tag){
Intent intent = new Intent(context,ViewTagActivity.class);
@ -82,13 +85,14 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn
fragmentTransaction.replace(R.id.fragment_container, fragment);
fragmentTransaction.commit();
ConstraintLayout quickTootContainer = findViewById(R.id.quick_toot_container);
QuickTootHelper quickTootHelper = new QuickTootHelper(this, quickTootContainer, accountManager, eventHub);
QuickTootViewModel quickTootViewModel = viewModelFactory.create(QuickTootViewModel.class);
QuickTootView quickTootView = findViewById(R.id.viewQuickToot);
quickTootView.attachViewModel(quickTootViewModel, this);
eventHub.getEvents()
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(quickTootHelper::handleEvent);
.subscribe(quickTootView::handleEvent);
}
@Override

View File

@ -23,4 +23,3 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
data class DomainMuteEvent(val instance: String): Dispatchable
data class QuickReplyEvent(val status: Status) : Dispatchable
data class StreamUpdateEvent(val status: Status, val targetKind: TimelineFragment.Kind, val targetIdentifier: String?, val first: Boolean) : Dispatchable
data class DrawerFooterClickedEvent(val placeholder: Boolean) : Dispatchable

View File

@ -17,6 +17,7 @@ import dagger.Binds
import dagger.MapKey
import dagger.Module
import dagger.multibindings.IntoMap
import net.accelf.yuito.QuickTootViewModel
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@ -85,5 +86,10 @@ abstract class ViewModelModule {
@ViewModelKey(ScheduledTootViewModel::class)
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(QuickTootViewModel::class)
internal abstract fun quickTootViewModel(viewModel: QuickTootViewModel): ViewModel
//Add more ViewModels here
}
}

View File

@ -1,27 +0,0 @@
package com.keylesspalace.tusky.entity
import android.text.Spanned
import com.google.gson.annotations.SerializedName
import java.util.*
data class Announcement(
val id: String,
val content: Spanned,
@SerializedName("starts_at") val startsAt: Date,
@SerializedName("ends_at") val endsAt: Date,
@SerializedName("all_day") val allDay: Boolean,
val emojis: List<Emoji>,
val mentions: Array<Status.Mention>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val announcement = other as Announcement?
return id == announcement?.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}

View File

@ -505,9 +505,6 @@ interface MastodonApi {
@Field("choices[]") choices: List<Int>
): Single<Poll>
@GET("api/v1/announcements")
fun listAnnouncements(): Single<List<Announcement>>
@POST("api/v1/accounts/{id}/block")
fun blockAccountObservable(
@Path("id") accountId: String

View File

@ -5,20 +5,15 @@ import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.entity.Instance
import com.mikepenz.materialdrawer.model.AbstractDrawerItem
import com.uber.autodispose.SingleSubscribeProxy
import kotlinx.android.synthetic.main.item_drawer_footer.view.*
class FooterDrawerItem : AbstractDrawerItem<FooterDrawerItem, FooterDrawerItem.ViewHolder>() {
override val type: Int
get() = R.id.instanceData
override val type = R.id.instanceData
override val layoutRes: Int
get() = R.layout.item_drawer_footer
lateinit var eventHub: EventHub
override val layoutRes = R.layout.item_drawer_footer
private lateinit var context: Context
private lateinit var instanceData: TextView
@ -29,24 +24,9 @@ class FooterDrawerItem : AbstractDrawerItem<FooterDrawerItem, FooterDrawerItem.V
instanceData = holder.instanceData
holder.itemView.setPadding(0, 0, 0, 0)
instanceData.setTextColor(instanceData.hintTextColors)
onDrawerItemClickListener = { _, _, _ ->
var result = true
var text = instanceData.text.toString()
text += "?"
if (text.endsWith("???????")) {
text = text.substring(0, text.length - 7)
eventHub.dispatch(DrawerFooterClickedEvent(true))
isExpanded = false
result = false
}
instanceData.text = text
result
}
}
override fun getViewHolder(v: View): ViewHolder {
return ViewHolder(v)
}
override fun getViewHolder(v: View) = ViewHolder(v)
fun setSubscribeProxy(subscribeProxy: SingleSubscribeProxy<Instance>) {
subscribeProxy.subscribe(
@ -60,6 +40,6 @@ class FooterDrawerItem : AbstractDrawerItem<FooterDrawerItem, FooterDrawerItem.V
}
class ViewHolder internal constructor(internal val view: View): RecyclerView.ViewHolder(view) {
internal val instanceData: TextView = view.findViewById(R.id.instanceData)
internal val instanceData = view.instanceData
}
}

View File

@ -1,346 +0,0 @@
package net.accelf.yuito;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import com.keylesspalace.tusky.AccountActivity;
import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
import com.keylesspalace.tusky.appstore.Event;
import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
import com.keylesspalace.tusky.components.compose.ComposeActivity;
import com.keylesspalace.tusky.components.compose.view.TootButton;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Announcement;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import io.reactivex.android.schedulers.AndroidSchedulers;
import static com.keylesspalace.tusky.components.compose.ComposeActivity.CAN_USE_UNLEAKABLE;
import static com.keylesspalace.tusky.components.compose.ComposeActivity.PREF_DEFAULT_TAG;
import static com.keylesspalace.tusky.components.compose.ComposeActivity.PREF_USE_DEFAULT_TAG;
import static com.uber.autodispose.AutoDispose.autoDisposable;
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
public class QuickTootHelper {
private Context context;
private TextView quickReplyInfo;
private TextView defaultTagInfo;
private ImageView visibilityButton;
private EditText tootEditText;
private ImageButton openAnnouncementsButton;
private TextView announcementsText;
private ImageButton prevButton;
private ImageButton nextButton;
private TextView announcementsCountText;
private TootButton quickTootButton;
private SharedPreferences defPrefs;
private String domain;
private String loggedInUsername;
private EventHub eventHub;
private LinkListener listener;
private Status inReplyTo;
private boolean open = false;
private int index = 0;
private List<Announcement> announcements;
private static final String PREF_CURRENT_VISIBILITY = "current_visibility";
public QuickTootHelper(BottomSheetActivity activity, ConstraintLayout root, AccountManager accountManager, EventHub eventHub) {
context = root.getContext();
quickReplyInfo = root.findViewById(R.id.quick_reply_info);
defaultTagInfo = root.findViewById(R.id.default_tag_info);
visibilityButton = root.findViewById(R.id.visibility_button);
tootEditText = root.findViewById(R.id.toot_edit_text);
openAnnouncementsButton = root.findViewById(R.id.button_open_announcements);
announcementsText = root.findViewById(R.id.text_view_announcements);
prevButton = root.findViewById(R.id.button_prev_announcements);
nextButton = root.findViewById(R.id.button_next_announcements);
announcementsCountText = root.findViewById(R.id.text_view_announcements_count);
quickTootButton = root.findViewById(R.id.toot_button);
context = root.getContext();
this.defPrefs = PreferenceManager.getDefaultSharedPreferences(context);
AccountEntity account = accountManager.getActiveAccount();
if (account != null) {
domain = account.getDomain();
loggedInUsername = account.getUsername();
}
this.eventHub = eventHub;
updateVisibilityButton();
updateDefaultTagInfo();
visibilityButton.setOnClickListener(v -> setNextVisibility());
quickTootButton.setOnClickListener(v -> quickToot());
listener = new LinkListener() {
@Override
public void onViewTag(String tag) {
context.startActivity(ViewTagActivity.getIntent(context, tag));
}
@Override
public void onViewAccount(String id) {
context.startActivity(AccountActivity.getIntent(context, id));
}
@Override
public void onViewUrl(String url, String text) {
activity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER, text);
}
};
activity.mastodonApi.listAnnouncements()
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(activity, Lifecycle.Event.ON_DESTROY)))
.subscribe(
a -> {
announcements = a;
updateAnnouncements();
},
Throwable::printStackTrace
);
updateAnnouncements();
openAnnouncementsButton.setOnClickListener(v -> toggleOpenAnnouncements());
announcementsText.setOnClickListener(v -> toggleOpenAnnouncements());
prevButton.setOnClickListener(v -> prevAnnouncement());
nextButton.setOnClickListener(v -> nextAnnouncement());
}
public void composeButton() {
if (tootEditText.getText().length() == 0 && inReplyTo == null) {
context.startActivity(getComposeIntent(context, true, false));
} else {
startComposeWithQuickComposeData();
}
}
public void handleEvent(Event event) {
if (event instanceof QuickReplyEvent) {
reply(((QuickReplyEvent) event).getStatus());
} else if (event instanceof PreferenceChangedEvent) {
switch (((PreferenceChangedEvent) event).getPreferenceKey()) {
case PREF_CURRENT_VISIBILITY: {
updateVisibilityButton();
break;
}
case PREF_DEFAULT_TAG:
case PREF_USE_DEFAULT_TAG: {
updateDefaultTagInfo();
break;
}
}
} else if (event instanceof DrawerFooterClickedEvent) {
tootEditText.setText("にゃーん");
}
}
private void reply(Status status) {
inReplyTo = status;
updateQuickReplyInfo();
}
private void startComposeWithQuickComposeData() {
Intent intent = getComposeIntent(context, false, false);
resetQuickCompose();
context.startActivity(intent);
}
private void quickToot() {
if (tootEditText.getText().toString().length() > 0) {
Intent intent = getComposeIntent(context, false, true);
resetQuickCompose();
context.startActivity(intent);
}
}
private Intent getComposeIntent(Context context, boolean onlyVisibility, boolean tootRightNow) {
ComposeActivity.ComposeOptions options = new ComposeActivity.ComposeOptions();
options.setVisibility(getCurrentVisibility());
if (onlyVisibility) {
return ComposeActivity.startIntent(context, options);
}
options.setTootText(tootEditText.getText().toString());
options.setTootRightNow(tootRightNow);
if (inReplyTo != null) {
Status.Mention[] mentions = inReplyTo.getMentions();
Set<String> mentionedUsernames = new LinkedHashSet<>();
mentionedUsernames.add(inReplyTo.getAccount().getUsername());
for (Status.Mention mention : mentions) {
mentionedUsernames.add(mention.getUsername());
}
mentionedUsernames.remove(loggedInUsername);
options.setInReplyToId(inReplyTo.getId());
options.setContentWarning(inReplyTo.getSpoilerText());
options.setMentionedUsernames(mentionedUsernames);
options.setReplyingStatusAuthor(inReplyTo.getAccount().getLocalUsername());
options.setReplyingStatusContent(inReplyTo.getContent().toString());
}
return ComposeActivity.startIntent(context, options);
}
private void resetQuickCompose() {
tootEditText.getText().clear();
inReplyTo = null;
updateQuickReplyInfo();
}
private void updateQuickReplyInfo() {
if (inReplyTo != null) {
quickReplyInfo.setText(String.format("Reply to : %s", inReplyTo.getAccount().getUsername()));
} else {
quickReplyInfo.setText("");
}
}
private void updateDefaultTagInfo() {
boolean useDefaultTag = defPrefs.getBoolean(PREF_USE_DEFAULT_TAG, false);
String defaultText = defPrefs.getString(PREF_DEFAULT_TAG, "");
if (useDefaultTag) {
defaultTagInfo.setText(String.format("%s : %s", context.getString(R.string.hint_default_text), defaultText));
defaultTagInfo.setTextColor(ThemeUtils.getColor(context, R.attr.colorInfo));
} else {
defaultTagInfo.setText(String.format("%s inactive", context.getString(R.string.hint_default_text)));
defaultTagInfo.setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary));
}
}
private Status.Visibility getCurrentVisibility() {
Status.Visibility visibility = Status.Visibility.byNum(defPrefs.getInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum()));
if (!Arrays.asList(CAN_USE_UNLEAKABLE)
.contains(domain) && visibility == Status.Visibility.UNLEAKABLE) {
defPrefs.edit()
.putInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum())
.apply();
eventHub.dispatch(new PreferenceChangedEvent(PREF_CURRENT_VISIBILITY));
return Status.Visibility.PUBLIC;
}
return visibility;
}
private void updateVisibilityButton() {
Status.Visibility visibility = getCurrentVisibility();
quickTootButton.setStatusVisibility(visibility);
switch (visibility) {
case PUBLIC:
visibilityButton.setImageResource(R.drawable.ic_public_24dp);
break;
case UNLISTED:
visibilityButton.setImageResource(R.drawable.ic_lock_open_24dp);
break;
case PRIVATE:
visibilityButton.setImageResource(R.drawable.ic_lock_outline_24dp);
break;
case UNLEAKABLE:
visibilityButton.setImageResource(R.drawable.ic_low_vision_24dp);
break;
}
}
private void setNextVisibility() {
Status.Visibility visibility = getCurrentVisibility();
switch (visibility) {
case PUBLIC:
visibility = Status.Visibility.UNLISTED;
break;
case UNLISTED:
visibility = Status.Visibility.PRIVATE;
break;
case PRIVATE:
if (Arrays.asList(CAN_USE_UNLEAKABLE).contains(domain)) {
visibility = Status.Visibility.UNLEAKABLE;
} else {
visibility = Status.Visibility.PUBLIC;
}
break;
case UNLEAKABLE:
case UNKNOWN:
visibility = Status.Visibility.PUBLIC;
break;
}
defPrefs.edit()
.putInt(PREF_CURRENT_VISIBILITY, visibility.getNum())
.apply();
eventHub.dispatch(new PreferenceChangedEvent(PREF_CURRENT_VISIBILITY));
updateVisibilityButton();
}
private void updateAnnouncements() {
if (ListUtils.isEmpty(announcements)) {
openAnnouncementsButton.setVisibility(View.GONE);
announcementsText.setVisibility(View.GONE);
announcementsCountText.setVisibility(View.GONE);
prevButton.setVisibility(View.GONE);
nextButton.setVisibility(View.GONE);
} else {
openAnnouncementsButton.setVisibility(View.VISIBLE);
announcementsText.setVisibility(View.VISIBLE);
announcementsCountText.setVisibility(View.VISIBLE);
if (open) {
prevButton.setVisibility(View.VISIBLE);
nextButton.setVisibility(View.VISIBLE);
} else {
prevButton.setVisibility(View.GONE);
nextButton.setVisibility(View.GONE);
}
openAnnouncementsButton.setImageDrawable(ContextCompat.getDrawable(context, open ? R.drawable.ic_arrow_drop_down : R.drawable.ic_arrow_drop_up));
announcementsText.setSingleLine(!open);
announcementsCountText.setText(String.format(Locale.getDefault(), "(%d/%d)", index + 1, announcements.size()));
Announcement announcement = announcements.get(index);
LinkHelper.setClickableText(announcementsText, announcement.getContent(), announcement.getMentions(), listener, false);
}
}
private void toggleOpenAnnouncements() {
open = !open;
updateAnnouncements();
}
private void prevAnnouncement() {
if (index > 0) {
index--;
updateAnnouncements();
}
}
private void nextAnnouncement() {
if (index < announcements.size() - 1) {
index++;
updateAnnouncements();
}
}
}

View File

@ -0,0 +1,98 @@
package net.accelf.yuito
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.Event
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.appstore.QuickReplyEvent
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.PREF_DEFAULT_TAG
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.PREF_USE_DEFAULT_TAG
import com.keylesspalace.tusky.util.ThemeUtils
import kotlinx.android.synthetic.main.view_quick_toot.view.*
class QuickTootView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
ConstraintLayout(context, attrs) {
private val preference by lazy { PreferenceManager.getDefaultSharedPreferences(context) }
private lateinit var viewModel: QuickTootViewModel
init {
LayoutInflater.from(context).inflate(R.layout.view_quick_toot, this, true)
}
fun attachViewModel(viewModel: QuickTootViewModel, owner: LifecycleOwner) {
this.viewModel = viewModel
buttonVisibility.attachViewModel(viewModel, owner)
viewModel.content.observe(owner) {
if (editTextContent.text.toString() != it) {
editTextContent.setText(it)
}
}
editTextContent.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.content.value = s.toString()
}
})
viewModel.inReplyTo.observe(owner) {
textQuickReply.text = it?.let { "Reply to ${it.account.username}" } ?: ""
}
viewModel.defaultTag.observe(owner) {
textDefaultTag.text = it?.let { "${context.getString(R.string.hint_default_text)} : $it" }
?: "${context.getString(R.string.hint_default_text)} inactive"
textDefaultTag.setTextColor(ThemeUtils.getColor(context, it?.let { R.attr.colorInfo }
?: android.R.attr.textColorTertiary))
}
syncDefaultTag()
viewModel.visibility.observe(owner) {
buttonToot.setStatusVisibility(it)
}
buttonToot.setOnClickListener {
val intent = ComposeActivity.startIntent(it.context, viewModel.composeOptions(true))
viewModel.reset()
it.context.startActivity(intent)
}
}
private fun syncDefaultTag() {
viewModel.defaultTag.value = if (preference.getBoolean(PREF_USE_DEFAULT_TAG, false)) {
preference.getString(PREF_DEFAULT_TAG, null)
} else {
null
}
}
fun onFABClicked(view: View) {
val intent = ComposeActivity.startIntent(view.context, viewModel.composeOptions(false))
viewModel.reset()
view.context.startActivity(intent)
}
fun handleEvent(event: Event?) {
when (event) {
is QuickReplyEvent -> viewModel.reply(event)
is PreferenceChangedEvent -> {
if (event.preferenceKey in arrayOf(PREF_DEFAULT_TAG, PREF_USE_DEFAULT_TAG)) {
syncDefaultTag()
}
}
}
}
}

View File

@ -0,0 +1,91 @@
package net.accelf.yuito
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.appstore.QuickReplyEvent
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.CAN_USE_UNLEAKABLE
import com.keylesspalace.tusky.components.compose.mutableLiveData
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Visibility
import javax.inject.Inject
class QuickTootViewModel @Inject constructor(
accountManager: AccountManager
): ViewModel() {
private val account = accountManager.activeAccount!!
private val unleakableAllowed by lazy { CAN_USE_UNLEAKABLE.contains(account.domain) }
val content = mutableLiveData("")
private val visibilityMutable = mutableLiveData(Visibility.PUBLIC)
val visibility: LiveData<Visibility> = visibilityMutable
private var stashedVisibility: Visibility? = null
private val inReplyToMutable: MutableLiveData<Status?> = mutableLiveData(null)
val inReplyTo: LiveData<Status?> = inReplyToMutable
val defaultTag: MutableLiveData<String?> = mutableLiveData(null)
fun setInitialVisibility(num: Int) {
visibilityMutable.value = (Visibility.byNum(num)
.takeUnless { it == Visibility.UNKNOWN }
?: account.defaultPostPrivacy)
.takeUnless { it == Visibility.UNLEAKABLE && unleakableAllowed }
?: Visibility.PRIVATE
}
fun stepVisibility() {
visibilityMutable.value = when (visibility.value) {
Visibility.PUBLIC -> Visibility.UNLISTED
Visibility.UNLISTED -> Visibility.PRIVATE
Visibility.PRIVATE -> when (unleakableAllowed) {
true -> Visibility.UNLEAKABLE
false -> Visibility.PUBLIC
}
Visibility.UNLEAKABLE -> Visibility.PUBLIC
else -> Visibility.PUBLIC
}
}
private fun overrideVisibility(overrideTo: Visibility) {
stashedVisibility = visibility.value
visibilityMutable.value = overrideTo
}
fun reply(event: QuickReplyEvent) {
val status = event.status.actionableStatus
inReplyToMutable.value = status
overrideVisibility(status.visibility)
}
fun composeOptions(tootRightNow: Boolean): ComposeActivity.ComposeOptions {
return ComposeActivity.ComposeOptions(
tootText = content.value,
mentionedUsernames = inReplyTo.value
?.let {
linkedSetOf(it.account.username, *(it.mentions.map { mention -> mention.username }.toTypedArray()))
.apply { remove(account.username) }
},
inReplyToId = inReplyTo.value?.id,
visibility = visibility.value,
contentWarning = inReplyTo.value?.spoilerText,
replyingStatusAuthor = inReplyTo.value?.account?.name,
replyingStatusContent = inReplyTo.value?.content?.toString(),
tootRightNow = tootRightNow
)
}
fun reset() {
content.value = ""
inReplyToMutable.value = null
stashedVisibility?.let {
visibilityMutable.value = stashedVisibility
stashedVisibility = null
}
}
}

View File

@ -0,0 +1,59 @@
package net.accelf.yuito
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Status.Visibility
class VisibilityToggleButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatImageView(context, attrs) {
private val preference by lazy { PreferenceManager.getDefaultSharedPreferences(context) }
init {
isClickable = true
isFocusable = true
}
fun attachViewModel(viewModel: QuickTootViewModel, owner: LifecycleOwner) {
viewModel.visibility.observe(owner, ::updateVisibility)
viewModel.setInitialVisibility(preference.getInt(PREF_CURRENT_VISIBILITY, Visibility.UNKNOWN.num))
setOnClickListener{ viewModel.stepVisibility() }
}
private fun updateVisibility(visibility: Visibility) {
setImageResource(
when (visibility) {
Visibility.PUBLIC -> R.drawable.ic_public_24dp
Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp
Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp
Visibility.DIRECT -> R.drawable.ic_email_24dp
Visibility.UNLEAKABLE -> R.drawable.ic_low_vision_24dp
else -> R.drawable.ic_lock_open_24dp
}
)
contentDescription = context.getString(
when (visibility) {
Visibility.UNKNOWN -> R.string.visibility_unknown
Visibility.PUBLIC -> R.string.visibility_public
Visibility.UNLISTED -> R.string.visibility_unlisted
Visibility.PRIVATE -> R.string.visibility_private
Visibility.DIRECT -> R.string.visibility_direct
Visibility.UNLEAKABLE -> R.string.visibility_unleakable
}
)
preference.edit()
.putInt(PREF_CURRENT_VISIBILITY, visibility.num)
.apply()
}
companion object {
private const val PREF_CURRENT_VISIBILITY = "current_visibility"
}
}

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,10l5,5 5,-5z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,14l5,-5 5,5z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

View File

@ -81,9 +81,8 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include
android:id="@+id/quickTootContainer"
layout="@layout/view_quick_toot"
<net.accelf.yuito.QuickTootView
android:id="@+id/viewQuickToot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0" />

View File

@ -35,11 +35,10 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include
android:id="@+id/quick_toot_container"
layout="@layout/view_quick_toot"
<net.accelf.yuito.QuickTootView
android:id="@+id/viewQuickToot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0" />
</LinearLayout>
</LinearLayout>

View File

@ -35,11 +35,10 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include
android:id="@+id/quick_toot_container"
layout="@layout/view_quick_toot"
<net.accelf.yuito.QuickTootView
android:id="@+id/viewQuickToot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
</LinearLayout>
</LinearLayout>

View File

@ -25,11 +25,10 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include
android:id="@+id/quick_toot_container"
layout="@layout/view_quick_toot"
<net.accelf.yuito.QuickTootView
android:id="@+id/viewQuickToot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0" />
</LinearLayout>
</LinearLayout>

View File

@ -5,101 +5,32 @@
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<ImageButton
android:id="@+id/button_open_announcements"
style="@style/TuskyImageButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:padding="4dp"
android:contentDescription="@string/action_more"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@+id/quick_reply_info"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_arrow_drop_up" />
<TextView
android:id="@+id/text_view_announcements"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@+id/quick_reply_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button_open_announcements" />
<ImageButton
android:id="@+id/button_prev_announcements"
style="@style/TuskyImageButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="4dp"
android:padding="4dp"
android:contentDescription="@string/action_more"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@+id/text_view_announcements_count"
app:layout_constraintEnd_toStartOf="@id/button_next_announcements"
app:srcCompat="@drawable/ic_chevron_left" />
<ImageButton
android:id="@+id/button_next_announcements"
style="@style/TuskyImageButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="4dp"
android:padding="4dp"
android:contentDescription="@string/action_more"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@+id/text_view_announcements_count"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_chevron_right" />
<TextView
android:id="@+id/text_view_announcements_count"
android:id="@+id/textQuickReply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@id/quick_reply_info"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/quick_reply_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/toot_edit_text"
app:layout_constraintBottom_toTopOf="@id/editTextContent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/default_tag_info"
android:id="@+id/textDefaultTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/toot_edit_text"
app:layout_constraintBottom_toTopOf="@id/editTextContent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/visibility_button"
<net.accelf.yuito.VisibilityToggleButton
android:id="@+id/buttonVisibility"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:clickable="true"
android:contentDescription="@null"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_public_24dp" />
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/toot_edit_text"
android:id="@+id/editTextContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autofillHints=""
@ -107,11 +38,11 @@
android:inputType="textMultiLine"
android:minHeight="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/toot_button"
app:layout_constraintStart_toEndOf="@id/visibility_button" />
app:layout_constraintEnd_toStartOf="@id/buttonToot"
app:layout_constraintStart_toEndOf="@id/buttonVisibility" />
<com.keylesspalace.tusky.components.compose.view.TootButton
android:id="@+id/toot_button"
android:id="@+id/buttonToot"
style="@style/TuskyButton"
android:layout_width="@dimen/toot_button_width"
android:layout_height="wrap_content"

View File

@ -224,6 +224,7 @@
<string name="visibility_private">Followers-Only: Post to followers only</string>
<string name="visibility_unleakable">Unleakable: Post to followed user by you only.</string>
<string name="visibility_direct">Direct: Post to mentioned users only</string>
<string name="visibility_unknown">Unknown visibility</string>
<string name="pref_title_edit_notification_settings">Notifications</string>
<string name="pref_title_notifications_enabled">Notifications</string>