~captainepoch/husky

af78abcc0abe2dafd622d625dc0110a7222131e5 — Adolfo Santiago a month ago 69230e1
Added support for ZWSP character for toots

This adds support for adding a ZWSP character in toots for non-spaced
custom emojis.

This fixes https://git.mentality.rip/FWGS/Husky/issues/113.
M husky/app/src/husky/res/values/strings.xml => husky/app/src/husky/res/values/strings.xml +19 -16
@@ 18,7 18,7 @@
    <string name="action_expand_menu">Expand menu</string>

    <string name="title_emoji_reacted_by">%s reacted by</string>
    

    <string name="hint_appname">Application name</string>
    <string name="hint_website">Application website</string>



@@ 44,6 44,9 @@
    <string name="pref_title_other">Other</string>
    <string name="pref_title_privacy">Privacy</string>

    <string name="pref_title_composing">Composing</string>
    <string name="pref_title_composing_title">Composing using zero-width space characters in emojis</string>

    <string name="pref_title_anonymize_upload_filenames">Anonymize uploaded file names</string>
    <string name="pref_title_live_notifications">Live notifications</string>
    <string name="pref_summary_live_notifications">May slightly increase power consumption</string>


@@ 67,7 70,7 @@

    <string name="streaming_notification_name">Live notifications</string>
    <string name="streaming_notification_description">Running live notifications for: </string>
    

    <!-- REPLACEMENT FOR TUSKY STRINGS -->
    <string name="action_toggle_visibility">Post visibility</string>
    <string name="action_schedule_toot">Schedule post</string>


@@ 83,49 86,49 @@
    <string name="reblog_private">Repeat to original audience</string>
    <string name="unreblog_private">Remove repeat</string>
    <string name="action_open_toot">Open post</string>
        

    <string name="compose_shortcut_long_label">Compose Post</string>
    

    <string name="description_status_reblogged">
        Repeated
    </string>
    <string name="dialog_delete_toot_warning">Delete this post?</string>
    <string name="dialog_redraft_toot_warning">Delete and re-draft this post?</string>
    

    <string name="error_sender_account_gone">Error sending post.</string>
        

    <string name="notification_reblog_format">%s repeated your post</string>
    <string name="notification_favourite_format">%s favorited your post</string>
    <string name="notification_boost_name">Repeats</string>
    <string name="notification_boost_description">Notifications when your posts get repeated</string>
    <string name="notification_favourite_description">Notifications when your posts get marked as favorite</string>
    

    <string name="pref_title_confirm_reblogs">Show confirmation dialog before repeating</string>
    <string name="pref_title_notification_filter_reblogs">my posts are repeated</string>
    <string name="pref_title_show_boosts">Show repeats</string>
    <string name="pref_title_alway_open_spoiler">Always expand posts marked with content warnings</string>
    

    <plurals name="reblogs">
        <item quantity="one">&lt;b>%s&lt;/b> Repeat</item>
        <item quantity="other">&lt;b>%s&lt;/b> Repeats</item>
    </plurals>    
    </plurals>
    <string name="send_status_link_to">Share post URL to…</string>
    <string name="send_status_content_to">Share post to…</string>
    <string name="send_toot_notification_title">Sending post…</string>
    <string name="send_toot_notification_error_title">Error sending post</string>
    <string name="send_toot_notification_channel_name">Sending posts</string>
    <string name="send_toot_notification_saved_content">A copy of the post has been saved to your drafts</string>
    

    <string name="status_share_content">Share content of post</string>
    <string name="status_share_link">Share link to post</string>
    <string name="status_boosted_format">%s repeated</string>
    <string name="status_replied_to_format">Reply to %s</string>
    

    <string name="title_scheduled_toot">Scheduled posts</string>
    <string name="title_reblogged_by">Repeated by</string>
    <string name="title_view_thread">Post</string>

    <!-- 
    <!--
    <string name="about_tusky_version">Husky %s</string>
    <string name="about_powered_by_tusky">Powered by Husky</string>
    <string name="about_tusky_license">Husky is free and open-source software.


@@ 136,7 139,7 @@
    We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom,
    to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html
    * the url can be changed to link to the localized version of the license.
    --> <!-- 
    --> <!--
    <string name="about_project_site">
        Project website:\n
        https://husky.fwgs.ru


@@ 150,7 153,7 @@
    <string name="license_description">Husky contains code and assets from the following open source projects:</string>
    <string name="add_account_description">Add new Fediverse Account</string>
    <string name="action_login">Login!</string>
    

    <string name="dialog_whats_an_instance">The address or domain of any instance can be entered
    here, such as shitposter.club, blob.cat, fedi.absturztau.be, expired.mentality.rip, and
    <a href="https://instances.social">more!</a>


@@ 160,7 163,7 @@
    you were on the same site.
    \n\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>.
    </string>
    

    <string name="warning_scheduling_interval">Mastodon/Pleroma has a minimum scheduling interval of 5 minutes.</string> -->
</resources>
        


M husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt => husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +16 -3
@@ 85,6 85,7 @@ import com.keylesspalace.tusky.components.common.toFileName
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.core.extensions.composeWithZwsp
import com.keylesspalace.tusky.core.extensions.viewBinding
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
import com.keylesspalace.tusky.db.AccountEntity


@@ 226,6 227,9 @@ class ComposeActivity : BaseActivity(),
            binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
        }

        viewModel.composeWithZwsp.value =
            preferences.getBoolean(PrefKeys.COMPOSING_ZWSP_CHAR, false)

        setupComposeField(viewModel.startingText)
        setupContentWarningField(composeOptions?.contentWarning)
        setupPollView()


@@ 264,7 268,6 @@ class ComposeActivity : BaseActivity(),
                        }
                    }
                } else if(type == "text/plain" && intent.action == Intent.ACTION_SEND) {

                    val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
                    val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
                    val shareBody = if(!subject.isNullOrBlank() && subject !in text) {


@@ 446,7 449,11 @@ class ComposeActivity : BaseActivity(),
            viewModel.instanceStickers.observe { stickers ->
                if(stickers.isNotEmpty()) {
                    binding.composeStickerButton.visibility = View.VISIBLE
                    enableButton(binding.composeStickerButton, true, true)
                    enableButton(
                        binding.composeStickerButton,
                        clickable = true,
                        colorActive = true
                    )
                    binding.stickerKeyboard.setupStickerKeyboard(this@ComposeActivity, stickers)
                }
            }


@@ 1146,7 1153,13 @@ class ComposeActivity : BaseActivity(),

    private fun sendStatus(preview: Boolean) {
        enableButtons(false)
        val contentText = binding.composeEditField.text.toString()
        val tempText = binding.composeEditField.text.toString()
        val contentText = if(viewModel.composeWithZwsp.value == true) {
            tempText.composeWithZwsp()
        } else {
            tempText
        }

        var spoilerText = ""
        if(viewModel.showContentWarning.value!!) {
            spoilerText = binding.composeContentWarningField.text.toString()

M husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt => husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +111 -97
@@ 1,32 1,34 @@
/* Copyright 2019 Tusky Contributors
/*
 * Husky -- A Pleroma client for Android
 *
 * This file is a part of Tusky.
 * Copyright (C) 2021  The Husky Developers
 * Copyright (C) 2019  Tusky Contributors
 *
 * 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.
 * 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.
 * This program 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>. */
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.components.compose

import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.keylesspalace.tusky.components.common.CommonComposeViewModel
import com.keylesspalace.tusky.components.common.MediaUploader
import com.keylesspalace.tusky.components.common.mutableLiveData
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Attachment


@@ 35,19 37,24 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.TootToSend
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.SaveTootHelper
import com.keylesspalace.tusky.util.combineLiveData
import com.keylesspalace.tusky.util.filter
import com.keylesspalace.tusky.util.map
import com.keylesspalace.tusky.util.randomAlphanumericString
import com.keylesspalace.tusky.util.toLiveData
import io.reactivex.Observable.just
import java.util.*
import java.util.ArrayList
import javax.inject.Inject

class ComposeViewModel @Inject constructor(
        private val api: MastodonApi,
        private val accountManager: AccountManager,
        private val mediaUploader: MediaUploader,
        private val serviceClient: ServiceClient,
        private val draftHelper: DraftHelper,
        private val saveTootHelper: SaveTootHelper,
        private val db: AppDatabase
    private val api: MastodonApi,
    private val accountManager: AccountManager,
    private val mediaUploader: MediaUploader,
    private val serviceClient: ServiceClient,
    private val draftHelper: DraftHelper,
    private val saveTootHelper: SaveTootHelper,
    private val db: AppDatabase
) : CommonComposeViewModel(api, accountManager, mediaUploader, db) {

    private var replyingStatusAuthor: String? = null


@@ 63,7 70,7 @@ class ComposeViewModel @Inject constructor(
    private var modifiedInitialState: Boolean = false

    val markMediaAsSensitive =
            mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
        mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)

    val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
    val showContentWarning = mutableLiveData(false)


@@ 71,6 78,7 @@ class ComposeViewModel @Inject constructor(
    val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
    val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
    val formattingSyntax: MutableLiveData<String> = mutableLiveData("")
    val composeWithZwsp: MutableLiveData<Boolean> = mutableLiveData(false)

    private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()



@@ 98,17 106,16 @@ class ComposeViewModel @Inject constructor(
    }

    fun deleteDraft() {
        if (savedTootUid != 0) {
        if(savedTootUid != 0) {
            saveTootHelper.deleteDraft(savedTootUid)
        }
        if (draftId != 0) {
        if(draftId != 0) {
            draftHelper.deleteDraftAndAttachments(draftId)
                    .subscribe()
                .subscribe()
        }
    }

    fun saveDraft(content: String, contentWarning: String) {

        val mediaUris: MutableList<String> = mutableListOf()
        val mediaDescriptions: MutableList<String?> = mutableListOf()
        media.value?.forEach { item ->


@@ 116,18 123,18 @@ class ComposeViewModel @Inject constructor(
            mediaDescriptions.add(item.description)
        }
        draftHelper.saveDraft(
                draftId = draftId,
                accountId = accountManager.activeAccount?.id!!,
                inReplyToId = inReplyToId,
                content = content,
                contentWarning = contentWarning,
                sensitive = markMediaAsSensitive.value!!,
                visibility = statusVisibility.value!!,
                mediaUris = mediaUris,
                mediaDescriptions = mediaDescriptions,
                poll = poll.value,
                formattingSyntax = formattingSyntax.value!!,
                failedToSend = false
            draftId = draftId,
            accountId = accountManager.activeAccount?.id!!,
            inReplyToId = inReplyToId,
            content = content,
            contentWarning = contentWarning,
            sensitive = markMediaAsSensitive.value!!,
            visibility = statusVisibility.value!!,
            mediaUris = mediaUris,
            mediaDescriptions = mediaDescriptions,
            poll = poll.value,
            formattingSyntax = formattingSyntax.value!!,
            failedToSend = false
        ).subscribe()
    }



@@ 137,60 144,60 @@ class ComposeViewModel @Inject constructor(
     * @return LiveData which will signal once the screen can be closed or null if there are errors
     */
    fun sendStatus(
            content: String,
            spoilerText: String,
            preview: Boolean
        content: String,
        spoilerText: String,
        preview: Boolean
    ): LiveData<Unit> {

        val deletionObservable = if (isEditingScheduledToot) {
        val deletionObservable = if(isEditingScheduledToot) {
            api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
        } else {
            just(Unit)
        }.toLiveData()

        val sendObservable = media
                .filter { items -> items.all { it.uploadPercent == -1 } }
                .map {
                    val mediaIds = ArrayList<String>()
                    val mediaUris = ArrayList<Uri>()
                    val mediaDescriptions = ArrayList<String>()
                    for (item in media.value!!) {
                        mediaIds.add(item.id!!)
                        mediaUris.add(item.uri)
                        mediaDescriptions.add(item.description ?: "")
                    }

                    val tootToSend = TootToSend(
                            text = content,
                            warningText = spoilerText,
                            visibility = statusVisibility.value!!.serverString(),
                            sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
                            mediaIds = mediaIds,
                            mediaUris = mediaUris.map { it.toString() },
                            mediaDescriptions = mediaDescriptions,
                            scheduledAt = scheduledAt.value,
                            inReplyToId = inReplyToId,
                            poll = poll.value,
                            replyingStatusContent = null,
                            replyingStatusAuthorUsername = null,
                            formattingSyntax = formattingSyntax.value!!,
                            preview = preview,
                            accountId = accountManager.activeAccount!!.id,
                            savedTootUid = savedTootUid,
                            draftId = draftId,
                            idempotencyKey = randomAlphanumericString(16),
                            retries = 0
                    )

                    serviceClient.sendToot(tootToSend)
            .filter { items -> items.all { it.uploadPercent == -1 } }
            .map {
                val mediaIds = ArrayList<String>()
                val mediaUris = ArrayList<Uri>()
                val mediaDescriptions = ArrayList<String>()
                for(item in media.value!!) {
                    mediaIds.add(item.id!!)
                    mediaUris.add(item.uri)
                    mediaDescriptions.add(item.description ?: "")
                }

                val tootToSend = TootToSend(
                    text = content,
                    warningText = spoilerText,
                    visibility = statusVisibility.value!!.serverString(),
                    sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
                    mediaIds = mediaIds,
                    mediaUris = mediaUris.map { it.toString() },
                    mediaDescriptions = mediaDescriptions,
                    scheduledAt = scheduledAt.value,
                    inReplyToId = inReplyToId,
                    poll = poll.value,
                    replyingStatusContent = null,
                    replyingStatusAuthorUsername = null,
                    formattingSyntax = formattingSyntax.value!!,
                    preview = preview,
                    accountId = accountManager.activeAccount!!.id,
                    savedTootUid = savedTootUid,
                    draftId = draftId,
                    idempotencyKey = randomAlphanumericString(16),
                    retries = 0
                )

                serviceClient.sendToot(tootToSend)
            }

        return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
    }

    fun setup(composeOptions: ComposeActivity.ComposeOptions?) {

        if (setupComplete.value == true) {
        if(setupComplete.value == true) {
            return
        }



@@ 198,17 205,18 @@ class ComposeViewModel @Inject constructor(

        val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
        startingVisibility = Status.Visibility.byNum(
                preferredVisibility.num.coerceAtLeast(replyVisibility.num))
            preferredVisibility.num.coerceAtLeast(replyVisibility.num)
        )

        inReplyToId = composeOptions?.inReplyToId

        modifiedInitialState = composeOptions?.modifiedInitialState == true

        val contentWarning = composeOptions?.contentWarning
        if (contentWarning != null) {
        if(contentWarning != null) {
            startingContentWarning = contentWarning
        }
        if (!contentWarningStateChanged) {
        if(!contentWarningStateChanged) {
            showContentWarning.value = !contentWarning.isNullOrBlank()
        }



@@ 216,22 224,27 @@ class ComposeViewModel @Inject constructor(
        val loadedDraftMediaUris = composeOptions?.mediaUrls
        val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
        val draftAttachments = composeOptions?.draftAttachments
        if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
        if(loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
            // when coming from SavedTootActivity
            loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
                    .forEach { (uri, description) ->
                        pickMedia(uri.toUri(), null).observeForever { errorOrItem ->
                            if (errorOrItem.isRight() && description != null) {
                                updateDescription(errorOrItem.asRight().localId, description)
                            }
                .forEach { (uri, description) ->
                    pickMedia(uri.toUri(), null).observeForever { errorOrItem ->
                        if(errorOrItem.isRight() && description != null) {
                            updateDescription(errorOrItem.asRight().localId, description)
                        }
                    }
        } else if (draftAttachments != null) {
                }
        } else if(draftAttachments != null) {
            // when coming from DraftActivity
            draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
            draftAttachments.forEach { attachment ->
                pickMedia(
                    attachment.uri,
                    attachment.description
                )
            }
        } else composeOptions?.mediaAttachments?.forEach { a ->
            // when coming from redraft or ScheduledTootActivity
            val mediaType = when (a.type) {
            val mediaType = when(a.type) {
                Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
                Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
                Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO


@@ 245,14 258,14 @@ class ComposeViewModel @Inject constructor(
        startingText = composeOptions?.tootText

        val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
        if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
        if(tootVisibility.num != Status.Visibility.UNKNOWN.num) {
            startingVisibility = tootVisibility
        }
        statusVisibility.value = startingVisibility
        val mentionedUsernames = composeOptions?.mentionedUsernames
        if (mentionedUsernames != null) {
        if(mentionedUsernames != null) {
            val builder = StringBuilder()
            for (name in mentionedUsernames) {
            for(name in mentionedUsernames) {
                builder.append('@')
                builder.append(name)
                builder.append(' ')


@@ 265,13 278,14 @@ class ComposeViewModel @Inject constructor(
        composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }

        val poll = composeOptions?.poll
        if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
        if(poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
            this.poll.value = poll
        }
        replyingStatusContent = composeOptions?.replyingStatusContent
        replyingStatusAuthor = composeOptions?.replyingStatusAuthor
        
        formattingSyntax.value = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax

        formattingSyntax.value = composeOptions?.formattingSyntax
            ?: accountManager.activeAccount!!.defaultFormattingSyntax
    }

    fun updatePoll(newPoll: NewPoll) {


@@ 283,7 297,7 @@ class ComposeViewModel @Inject constructor(
    }

    override fun onCleared() {
        for (uploadDisposable in mediaToDisposable.values) {
        for(uploadDisposable in mediaToDisposable.values) {
            uploadDisposable.dispose()
        }
        super.onCleared()

M husky/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt => husky/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +66 -26
@@ 1,17 1,22 @@
/* Copyright 2018 Conny Duck
/*
 * Husky -- A Pleroma client for Android
 *
 * This file is a part of Tusky.
 * Copyright (C) 2021  The Husky Developers
 * Copyright (C) 2018  Conny Duck
 *
 * 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.
 * 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.
 * This program 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>. */
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.components.preference



@@ 22,7 27,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.settings.AppTheme
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.emojiPreference
import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString


@@ 31,8 43,8 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
import okhttp3.OkHttpClient
import javax.inject.Inject
import okhttp3.OkHttpClient

class PreferencesFragment : PreferenceFragmentCompat(), Injectable {



@@ 153,9 165,15 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                    isSingleLineTitle = false
                    setOnPreferenceClickListener {
                        activity?.let { activity ->
                            val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
                            val intent = PreferencesActivity.newIntent(
                                activity,
                                PreferencesActivity.TAB_FILTER_PREFERENCES
                            )
                            activity.startActivity(intent)
                            activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
                            activity.overridePendingTransition(
                                R.anim.slide_from_right,
                                R.anim.slide_to_left
                            )
                        }
                        true
                    }


@@ 181,14 199,14 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                    setTitle(R.string.pref_title_enable_swipe_for_tabs)
                    isSingleLineTitle = false
                }
                

                switchPreference {
                    setDefaultValue(true)
                    key = PrefKeys.BIG_EMOJIS
                    setTitle(R.string.pref_title_enable_big_emojis)
                    isSingleLineTitle = false
                }
                

                switchPreference {
                    setDefaultValue(false)
                    key = PrefKeys.STICKERS


@@ 211,6 229,15 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                }
            }

            preferenceCategory(R.string.pref_title_composing) {
                switchPreference {
                    setDefaultValue(false)
                    key = PrefKeys.COMPOSING_ZWSP_CHAR
                    setTitle(R.string.pref_title_composing_title)
                    isSingleLineTitle = false
                }
            }

            preferenceCategory(R.string.pref_title_privacy) {
                switchPreference {
                    setDefaultValue(false)


@@ 234,9 261,15 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                    setTitle(R.string.pref_title_status_tabs)
                    setOnPreferenceClickListener {
                        activity?.let { activity ->
                            val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
                            val intent = PreferencesActivity.newIntent(
                                activity,
                                PreferencesActivity.TAB_FILTER_PREFERENCES
                            )
                            activity.startActivity(intent)
                            activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
                            activity.overridePendingTransition(
                                R.anim.slide_from_right,
                                R.anim.slide_to_left
                            )
                        }
                        true
                    }


@@ 249,10 282,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                    setDefaultValue(false)
                    key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
                    setOnPreferenceChangeListener { _, value ->
                        for (account in accountManager.accounts) {
                            val notificationFilter = deserialize(account.notificationsFilter).toMutableSet()
                        for(account in accountManager.accounts) {
                            val notificationFilter =
                                deserialize(account.notificationsFilter).toMutableSet()

                            if (value == true) {
                            if(value == true) {
                                notificationFilter.add(Notification.Type.FAVOURITE)
                                notificationFilter.add(Notification.Type.FOLLOW)
                                notificationFilter.add(Notification.Type.REBLOG)


@@ 287,9 321,15 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
                    setTitle(R.string.pref_title_http_proxy_settings)
                    setOnPreferenceClickListener {
                        activity?.let { activity ->
                            val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.PROXY_PREFERENCES)
                            val intent = PreferencesActivity.newIntent(
                                activity,
                                PreferencesActivity.PROXY_PREFERENCES
                            )
                            activity.startActivity(intent)
                            activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
                            activity.overridePendingTransition(
                                R.anim.slide_from_right,
                                R.anim.slide_to_left
                            )
                        }
                        true
                    }


@@ 319,13 359,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {

        try {
            val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
                    .toInt()
                .toInt()

            if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
            if(httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
                httpProxyPref?.summary = "$httpServer:$httpPort"
                return
            }
        } catch (e: NumberFormatException) {
        } catch(e: NumberFormatException) {
            // user has entered wrong port, fall back to empty summary
        }


A husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/CharExt.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/CharExt.kt +29 -0
@@ 0,0 1,29 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.extensions

/**
 * Returns if a char is a breakline or not.
 *
 * @return True if it is a breakline, False otherwise.
 */
fun Char.isBreakline(): Boolean {
    return (this == '\n')
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/StringExt.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/StringExt.kt +51 -0
@@ 0,0 1,51 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.extensions

import java.util.regex.Pattern

/**
 * Returns the text with emojis and zero-width space characters at the start and end positions.
 *
 * @return String with zero-width space characters at start and end positions for emojis.
 */
fun String.composeWithZwsp(): String {
    val zwspChar = '\u200b'
    val pattern = Pattern.compile("(:)([a-zA-Z0-9_]*)(:( )?(\\R)?)")
    val matcher = pattern.matcher(this)

    var end: Int
    val originalString = StringBuilder(this)
    while(matcher.find()) {
        end = matcher.end()

        if(end < originalString.length) {
            val endChar = originalString[end - 1]

            if(endChar.isWhitespace()) {
                if(!originalString[end].isLetterOrDigit() && !endChar.isBreakline()) {
                    originalString.setCharAt(end - 1, zwspChar)
                }
            }
        }
    }

    return originalString.toString().trim()
}

M husky/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt => husky/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +21 -0
@@ 1,3 1,23 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 * Copyright (C) 2020  Tusky Contributors
 *
 * 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.settings

enum class AppTheme(val value: String) {


@@ 37,6 57,7 @@ object PrefKeys {
    const val HIDE_MUTED_USERS = "hideMutedUsers"
    const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis"
    const val RENDER_STATUS_AS_MENTION = "renderStatusAsMention"
    const val COMPOSING_ZWSP_CHAR = "composingZwspChar"

    const val CUSTOM_TABS = "customTabs"
    const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"