Compare commits
13 Commits
c4b7296bfb
...
64d7e263fe
Author | SHA1 | Date |
---|---|---|
Hararan | 64d7e263fe | |
Alibek Omarov | d16c7f08ab | |
Alibek Omarov | 3c1b82056c | |
Alibek Omarov | 3e9c568e76 | |
Alibek Omarov | c062e9c180 | |
Alibek Omarov | 78352944c1 | |
Alibek Omarov | 6ff14af290 | |
Alibek Omarov | 9f4f805141 | |
Alibek Omarov | d68260ba4e | |
Alibek Omarov | b495462b33 | |
Konrad Pozniak | 9680bbf82b | |
Ivan Kupalov | 762be59036 | |
Alibek Omarov | b3259fcfd9 |
|
@ -1,7 +1,8 @@
|
|||
# Husky
|
||||
[![Build Status](https://api.travis-ci.org/FWGS/Husky.svg?branch=develop)](https://travis-ci.org/FWGS/Husky)\
|
||||
|
||||
[![Download F-Droid](https://img.shields.io/badge/download-fdroid-blue)](https://f-droid.org/repository/browse/?fdid=su.xash.husky) [![Download Google Play](https://img.shields.io/badge/download-googleplay-blue)](https://play.google.com/store/apps/details?id=su.xash.husky) [![Download Testing](https://img.shields.io/badge/downloads-testing-green)](https://github.com/FWGS/Husky/releases/tag/continuous)
|
||||
[![Download F-Droid](https://img.shields.io/badge/download-fdroid-blue)](https://f-droid.org/repository/browse/?fdid=su.xash.husky)\
|
||||
[![Download Google Play](https://img.shields.io/badge/download-googleplay-blue)](https://play.google.com/store/apps/details?id=su.xash.husky)\
|
||||
[![Download Testing](https://img.shields.io/badge/downloads-testing-green)](https://github.com/FWGS/Husky/releases/tag/continuous)
|
||||
|
||||
![icon](https://git.mentality.rip/FWGS/Husky/raw/branch/develop/assets/splash.xcf)
|
||||
|
||||
|
@ -20,12 +21,14 @@ Tusky is quote, unquote, `... a beautiful Android client for [Mastodon](https://
|
|||
- Support for seen notifications to less annoy you
|
||||
- "Reply to" feature that allows to jump to replied status, useful for hellthreading ;)
|
||||
- Bigger emojis!
|
||||
- "Preview" feature on Pleroma
|
||||
|
||||
### Support
|
||||
|
||||
If you have any bug reports, feature requests or questions please open an issue or send us a post at [Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)!
|
||||
|
||||
For translating Tusky into your language, visit https://weblate.tusky.app/. For translating Husky, translate app/src/main/res/values/husky.xml file and send it to profile above or send a pull request.
|
||||
For translating Tusky into your language, visit https://weblate.tusky.app/.\
|
||||
For translating Husky, visit https://l10n.mentality.rip.
|
||||
|
||||
### Head of development
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ dependencies {
|
|||
|
||||
implementation "com.github.connyduck:sparkbutton:4.0.0"
|
||||
|
||||
implementation "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.1'
|
||||
|
||||
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pref_title_default_formatting">ไวยากรณ์การจัดรูปแบบเริ่มต้น (ถ้ารองรับโดย Instance)</string>
|
||||
<string name="pref_title_default_formatting">ค่าปริยายของไวยากรณ์การจัดรูปแบบ (ถ้ารองรับโดย Instance)</string>
|
||||
<string name="pref_title_enable_big_emojis">เปิดใช้งานเอโมจิที่กำหนดเองขนาดใหญ่</string>
|
||||
<string name="action_send">โพสต์</string>
|
||||
<string name="action_send_public">โพสต์!</string>
|
||||
|
@ -15,7 +15,7 @@
|
|||
<string name="action_hide_reblogs">ซ่อนรีพีต</string>
|
||||
<string name="unreblog_private">ลบรีพีต</string>
|
||||
<string name="description_status_reblogged">รีพีตแล้ว</string>
|
||||
<string name="pref_title_hide_muted_users">ซ่อนผู้ใช้ที่ถูกทำให้เป็นใบ้ไว้</string>
|
||||
<string name="pref_title_hide_muted_users">ซ่อนผู้ใช้ที่ปิดเสียงไว้</string>
|
||||
<string name="action_toggle_visibility">การมองเห็นโพสต์</string>
|
||||
<string name="action_schedule_toot">โพสต์แบบกำหนดเวลา</string>
|
||||
<string name="notification_boost_name">รีพีต</string>
|
||||
|
@ -30,7 +30,7 @@
|
|||
<string name="send_toot_notification_title">กำลังส่งโพสต์…</string>
|
||||
<string name="send_toot_notification_error_title">เกิดข้อผิดพลาดในการส่งโพสต์</string>
|
||||
<string name="status_share_content">แบ่งปันเนื้อหาของโพสต์</string>
|
||||
<string name="status_boosted_format">%s ถูกรีพีต</string>
|
||||
<string name="status_boosted_format">%s ได้รีพีต</string>
|
||||
<string name="notification_emoji_name">การโต้ตอบเอโมจิ</string>
|
||||
<string name="send_status_link_to">แบ่งปัน URL โพสต์ไป…</string>
|
||||
<string name="title_view_thread">โพสต์</string>
|
||||
|
|
|
@ -83,6 +83,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
private var followState: FollowState = FollowState.NOT_FOLLOWING
|
||||
private var blocking: Boolean = false
|
||||
private var muting: Boolean = false
|
||||
private var blockingDomain: Boolean = false
|
||||
private var showingReblogs: Boolean = false
|
||||
private var subscribing: Boolean = false
|
||||
private var loadedAccount: Account? = null
|
||||
|
@ -543,6 +544,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
blocking = relation.blocking
|
||||
muting = relation.muting
|
||||
blockingDomain = relation.blockingDomain
|
||||
showingReblogs = relation.showingReblogs
|
||||
|
||||
accountFollowsYouTextView.visible(relation.followedBy)
|
||||
|
@ -664,10 +666,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
if (domain.isEmpty()) {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
} else {
|
||||
if (blockingDomain) {
|
||||
muteDomain.title = getString(R.string.action_unmute_domain, domain)
|
||||
} else {
|
||||
muteDomain.title = getString(R.string.action_mute_domain, domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (followState == FollowState.FOLLOWING) {
|
||||
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
||||
|
@ -710,13 +716,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun showMuteDomainWarningDialog(instance: String) {
|
||||
private fun toggleBlockDomain(instance: String) {
|
||||
if(blockingDomain) {
|
||||
viewModel.unblockDomain(instance)
|
||||
} else {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.muteDomain(instance) }
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBlock() {
|
||||
if (viewModel.relationshipData.value?.data?.blocking != true) {
|
||||
|
@ -796,7 +806,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
return true
|
||||
}
|
||||
R.id.action_mute_domain -> {
|
||||
showMuteDomainWarningDialog(domain)
|
||||
toggleBlockDomain(domain)
|
||||
return true
|
||||
}
|
||||
R.id.action_show_reblogs -> {
|
||||
|
|
|
@ -142,6 +142,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
|
||||
toolbar.setOnMenuItemClickListener { item: MenuItem ->
|
||||
when (item.itemId) {
|
||||
R.id.action_open_in_external_app -> openInExternalApp()
|
||||
R.id.action_download -> requestDownloadMedia()
|
||||
R.id.action_open_status -> onOpenStatus()
|
||||
R.id.action_share_media -> shareMedia()
|
||||
|
@ -269,6 +270,19 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
||||
}
|
||||
|
||||
private fun openInExternalApp() {
|
||||
val url = attachments!![viewPager.currentItem].attachment.url
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
if(extension != null) {
|
||||
intent.setDataAndType(Uri.parse(url), MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension))
|
||||
} else {
|
||||
intent.data = Uri.parse(url)
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
private var isCreating: Boolean = false
|
||||
|
||||
|
|
|
@ -114,10 +114,6 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi
|
|||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_open_in_web: {
|
||||
LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_reveal: {
|
||||
fragment.onRevealPressed();
|
||||
return true;
|
||||
|
|
|
@ -54,6 +54,7 @@ import androidx.core.view.inputmethod.InputContentInfoCompat
|
|||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -97,6 +98,8 @@ import kotlin.collections.ArrayList
|
|||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import me.thanel.markdownedit.MarkdownEdit
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
|
||||
class ComposeActivity : BaseActivity(),
|
||||
ComposeOptionsListener,
|
||||
|
@ -198,7 +201,9 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
stickerKeyboard.isSticky = true
|
||||
|
||||
eventHub.events.subscribe { event ->
|
||||
eventHub.events.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event: Event? ->
|
||||
when(event) {
|
||||
is StatusPreviewEvent -> onStatusPreviewReady(event.status)
|
||||
}
|
||||
|
@ -967,9 +972,8 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
private fun onSendClicked(preview: Boolean) {
|
||||
if(preview && previewBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
if(preview && previewBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
|
||||
previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
return
|
||||
}
|
||||
|
||||
if (verifyScheduledTime()) {
|
||||
|
@ -988,8 +992,6 @@ class ComposeActivity : BaseActivity(),
|
|||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
// Log.d("ComposeActivityPreview", "Preview: " + status.content)
|
||||
}
|
||||
|
||||
/** This is for the fancy keyboards which can insert images and stuff. */
|
||||
|
@ -1030,11 +1032,11 @@ class ComposeActivity : BaseActivity(),
|
|||
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||
getString(R.string.dialog_message_uploading_media), true, true)
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText, preview).observe(this, Observer {
|
||||
viewModel.sendStatus(contentText, spoilerText, preview).observeOnce(this) {
|
||||
finishingUploadDialog?.dismiss()
|
||||
if(!preview)
|
||||
deleteDraftAndFinish()
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
|
|
|
@ -33,9 +33,9 @@ import at.connyduck.sparkbutton.helpers.Utils
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
|
||||
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
|
||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
|
||||
|
@ -50,9 +50,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
dialogLayout.setPadding(padding, padding, padding, padding)
|
||||
|
||||
dialogLayout.orientation = LinearLayout.VERTICAL
|
||||
val imageView = PhotoView(this).apply {
|
||||
// If it seems a lot, try opening an image of A4 format or similar
|
||||
maximumScale = 6.0f
|
||||
val imageView = TouchImageView(this).apply {
|
||||
maxZoom = 6f
|
||||
}
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.keylesspalace.tusky.entity.Status.Mention
|
|||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
|
@ -194,6 +195,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
}
|
||||
|
||||
private fun onShowReplyTo(replyToId: String) {
|
||||
bottomSheetActivity?.viewThread(replyToId, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = SearchStatusesFragment()
|
||||
}
|
||||
|
@ -267,6 +272,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
openAsItem.title = openAsTitle
|
||||
|
||||
if(status.inReplyToId == null) {
|
||||
popup.menu.findItem(R.id.status_reply_to)?.isVisible = false
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.status_share_content -> {
|
||||
|
@ -296,6 +305,14 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl))
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_open_in_web -> {
|
||||
LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), context);
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_reply_to -> {
|
||||
onShowReplyTo(status.inReplyToId!!)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_open_as -> {
|
||||
showOpenAsDialog(statusUrl!!, item.title)
|
||||
return@setOnMenuItemClickListener true
|
||||
|
@ -334,10 +351,6 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
showConfirmDeleteDialog(id, position)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_delete_and_redraft -> {
|
||||
showConfirmEditDialog(id, position, status)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.pin -> {
|
||||
viewModel.pinAccount(status, !status.isPinned())
|
||||
return@setOnMenuItemClickListener true
|
||||
|
|
|
@ -31,7 +31,7 @@ data class Instance (
|
|||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||
@SerializedName("max_bio_chars") val maxBioChars: Int?,
|
||||
@SerializedName("poll_limits") val pollLimits: PollLimits?,
|
||||
val pleroma: InstancePleroma
|
||||
val pleroma: InstancePleroma?
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
return uri.hashCode()
|
||||
|
|
|
@ -25,5 +25,6 @@ data class Relationship (
|
|||
val muting: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
val subscribing: Boolean? = null // Pleroma extension
|
||||
val subscribing: Boolean? = null, // Pleroma extension
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean
|
||||
)
|
||||
|
|
|
@ -62,6 +62,7 @@ import com.keylesspalace.tusky.entity.Status;
|
|||
import com.keylesspalace.tusky.entity.EmojiReaction;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.network.TimelineCases;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
||||
|
@ -320,6 +321,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
clipboard.setPrimaryClip(clip);
|
||||
return true;
|
||||
}
|
||||
case R.id.status_open_in_web: {
|
||||
LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), getContext());
|
||||
return true;
|
||||
}
|
||||
case R.id.status_reply_to: {
|
||||
onShowReplyTo(status.getInReplyToId());
|
||||
return true;
|
||||
|
@ -364,10 +369,6 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
showConfirmDeleteDialog(id, position);
|
||||
return true;
|
||||
}
|
||||
case R.id.status_delete_and_redraft: {
|
||||
showConfirmEditDialog(id, position, status);
|
||||
return true;
|
||||
}
|
||||
case R.id.pin: {
|
||||
timelineCases.pin(status, !status.isPinned());
|
||||
return true;
|
||||
|
|
|
@ -21,9 +21,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
|
@ -31,7 +29,6 @@ import com.bumptech.glide.load.DataSource
|
|||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.github.chrisbanes.photoview.PhotoViewAttacher
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
|
@ -48,11 +45,11 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
fun onPhotoTap()
|
||||
}
|
||||
|
||||
private lateinit var attacher: PhotoViewAttacher
|
||||
private lateinit var photoActionsListener: PhotoActionsListener
|
||||
private lateinit var toolbar: View
|
||||
private var transition = BehaviorSubject.create<Unit>()
|
||||
private var shouldStartTransition = false
|
||||
|
||||
// Volatile: Image requests happen on background thread and we want to see updates to it
|
||||
// immediately on another thread. Atomic is an overkill for such thing.
|
||||
@Volatile
|
||||
|
@ -67,23 +64,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
override fun setupMediaView(url: String, previewUrl: String?) {
|
||||
descriptionView = mediaDescription
|
||||
photoView.transitionName = url
|
||||
attacher = PhotoViewAttacher(photoView).apply {
|
||||
// Clicking outside the photo closes the viewer.
|
||||
setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
|
||||
setOnClickListener { onMediaTap() }
|
||||
|
||||
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo
|
||||
* mostly fills the screen so clicking outside is difficult. */
|
||||
setOnSingleFlingListener { _, _, velocityX, velocityY ->
|
||||
var result = false
|
||||
if (abs(velocityY) > abs(velocityX)) {
|
||||
photoActionsListener.onDismiss()
|
||||
result = true
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
startedTransition = false
|
||||
loadImageFromNetwork(url, previewUrl, photoView)
|
||||
}
|
||||
|
@ -94,10 +74,66 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val arguments = this.arguments!!
|
||||
val gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
onMediaTap()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
var lastY = 0f
|
||||
photoView.setOnTouchListener { _, event ->
|
||||
// This part is for scaling/translating on vertical move.
|
||||
// We use raw coordinates to get the correct ones during scaling
|
||||
var result = true
|
||||
|
||||
gestureDetector.onTouchEvent(event)
|
||||
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
lastY = event.rawY
|
||||
} else if (!photoView.isZoomed && event.action == MotionEvent.ACTION_MOVE) {
|
||||
val diff = event.rawY - lastY
|
||||
// This code is to prevent transformations during page scrolling
|
||||
// If we are already translating or we reached the threshold, then transform.
|
||||
if (photoView.translationY != 0f || abs(diff) > 40) {
|
||||
photoView.translationY += (diff)
|
||||
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
|
||||
photoView.scaleY = scale
|
||||
photoView.scaleX = scale
|
||||
lastY = event.rawY
|
||||
}
|
||||
return@setOnTouchListener true
|
||||
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||
onGestureEnd()
|
||||
} else if (event.pointerCount >= 2 || photoView.canScrollHorizontally(1) && photoView.canScrollHorizontally(-1)) {
|
||||
// Starting from here is adapted code from TouchImageView to play nice with pager.
|
||||
|
||||
// Can scroll horizontally checks if there's still a part of the image.
|
||||
// That can be scrolled until you reach the edge multi-touch event.
|
||||
val parent = view.parent
|
||||
result = when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
// Disallow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
// Disable touch on view
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Allow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
val arguments = this.requireArguments()
|
||||
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
||||
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
||||
val url: String?
|
||||
|
@ -116,6 +152,14 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
finalizeViewSetup(url, attachment?.previewUrl, description)
|
||||
}
|
||||
|
||||
private fun onGestureEnd() {
|
||||
if (abs(photoView.translationY) > 180) {
|
||||
photoActionsListener.onDismiss()
|
||||
} else {
|
||||
photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMediaTap() {
|
||||
photoActionsListener.onPhotoTap()
|
||||
}
|
||||
|
@ -155,7 +199,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
.load(previewUrl)
|
||||
.dontAnimate()
|
||||
.onlyRetrieveFromCache(true)
|
||||
.centerInside()
|
||||
.addListener(ImageRequestListener(true, isThumnailRequest = true)))
|
||||
else it
|
||||
}
|
||||
|
@ -164,7 +207,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
.centerInside()
|
||||
.addListener(ImageRequestListener(false, isThumnailRequest = false))
|
||||
)
|
||||
.centerInside()
|
||||
.addListener(ImageRequestListener(true, isThumnailRequest = false))
|
||||
.into(photoView)
|
||||
}
|
||||
|
@ -225,13 +267,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
// another branch. take() will unsubscribe after we have it to not leak menmory
|
||||
transition
|
||||
.take(1)
|
||||
.subscribe {
|
||||
target.onResourceReady(resource, null)
|
||||
// It's needed. Don't ask why, I don't know, setImageDrawable() should
|
||||
// do it by itself but somehow it doesn't work automatically.
|
||||
// Just do it. If you don't, image will jump around when touched.
|
||||
attacher.update()
|
||||
}
|
||||
.subscribe { target.onResourceReady(resource, null) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.content.Intent
|
|||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -130,8 +131,8 @@ class SendTootService : Service(), Injectable {
|
|||
|
||||
tootToSend.retries++
|
||||
|
||||
val contentType : String? = if(tootToSend.formattingSyntax.length == 0) null else tootToSend.formattingSyntax
|
||||
val preview : Boolean? = if(tootToSend.preview) tootToSend.preview else null
|
||||
val contentType : String? = if(tootToSend.formattingSyntax.isNotEmpty()) tootToSend.formattingSyntax else null
|
||||
val preview : Boolean? = if(tootToSend.preview) true else null
|
||||
|
||||
val newStatus = NewStatus(
|
||||
tootToSend.text,
|
||||
|
@ -167,14 +168,11 @@ class SendTootService : Service(), Injectable {
|
|||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||
}
|
||||
|
||||
if (tootToSend.preview) {
|
||||
response.body()?.let(::StatusPreviewEvent)?.let(eventHub::dispatch)
|
||||
} else if (scheduled) {
|
||||
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||
} else {
|
||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||
when {
|
||||
tootToSend.preview -> response.body()?.let(::StatusPreviewEvent)?.let(eventHub::dispatch)
|
||||
scheduled -> response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||
else -> response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||
}
|
||||
|
||||
notificationManager.cancel(tootId)
|
||||
|
||||
} else {
|
||||
|
|
|
@ -27,6 +27,24 @@ inline fun <X, Y> LiveData<X>.switchMap(
|
|||
crossinline switchMapFunction: (X) -> LiveData<Y>
|
||||
): LiveData<Y> = Transformations.switchMap(this) { input -> switchMapFunction(input) }
|
||||
|
||||
fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) {
|
||||
observeForever(object: Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
removeObserver(this)
|
||||
observer(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.observeOnce(owner: LifecycleOwner, observer: (T) -> Unit) {
|
||||
observe(owner, object: Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
removeObserver(this)
|
||||
observer(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inline fun <X> LiveData<X>.filter(crossinline predicate: (X) -> Boolean): LiveData<X> {
|
||||
val liveData = MediatorLiveData<X>()
|
||||
liveData.addSource(this) { value ->
|
||||
|
|
|
@ -164,18 +164,41 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun muteDomain(instance: String) {
|
||||
fun blockDomain(instance: String) {
|
||||
mastodonApi.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
eventHub.dispatch(DomainMuteEvent(instance))
|
||||
val relation = relationshipData.value?.data
|
||||
if(relation != null) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = true)))
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, String.format("Error muting %s", instance))
|
||||
Log.e(TAG, "Error muting %s".format(instance))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, String.format("Error muting %s", instance), t)
|
||||
Log.e(TAG, "Error muting %s".format(instance), t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unblockDomain(instance: String) {
|
||||
mastodonApi.unblockDomain(instance).enqueue(object: Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
val relation = relationshipData.value?.data
|
||||
if(relation != null) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = false)))
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Error unmuting %s".format(instance))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error unmuting %s".format(instance), t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,3H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.11,3 19,3zM19,19H5V7h14V19zM13.5,13c0,0.83 -0.67,1.5 -1.5,1.5s-1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5S13.5,12.17 13.5,13zM12,9c-2.73,0 -5.06,1.66 -6,4c0.94,2.34 3.27,4 6,4s5.06,-1.66 6,-4C17.06,10.66 14.73,9 12,9zM12,15.5c-1.38,0 -2.5,-1.12 -2.5,-2.5c0,-1.38 1.12,-2.5 2.5,-2.5c1.38,0 2.5,1.12 2.5,2.5C14.5,14.38 13.38,15.5 12,15.5z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -262,7 +262,7 @@
|
|||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="60dp"
|
||||
android:paddingBottom="@dimen/compose_activity_bottom_bar_height"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
@ -276,7 +276,7 @@
|
|||
android:paddingStart="24dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="60dp"
|
||||
android:paddingBottom="@dimen/compose_activity_bottom_bar_height"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
@ -290,7 +290,7 @@
|
|||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="52dp"
|
||||
android:paddingBottom="@dimen/compose_activity_bottom_bar_height"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
@ -301,19 +301,20 @@
|
|||
android:layout_height="300dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:elevation="12dp"
|
||||
android:paddingBottom="60dp"
|
||||
android:paddingBottom="@dimen/compose_activity_bottom_bar_height"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/previewScroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="300dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:elevation="12dp"
|
||||
android:paddingBottom="60dp"
|
||||
android:paddingBottom="@dimen/compose_activity_bottom_bar_height"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_skipCollapsed="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
|
||||
>
|
||||
|
@ -323,7 +324,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
/>
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -459,7 +460,7 @@
|
|||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
app:icon="@drawable/ic_eye_24dp"
|
||||
app:icon="@drawable/ic_preview_24dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:visibility="gone"
|
||||
android:layout_toLeftOf="@+id/composeTootButton"
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
<com.ortiz.touchview.TouchImageView
|
||||
android:id="@+id/photoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
<item
|
||||
android:id="@+id/status_copy_link"
|
||||
android:title="@string/action_copy_link" />
|
||||
<item
|
||||
android:id="@+id/status_open_in_web"
|
||||
android:title="@string/action_open_in_web" />
|
||||
<item
|
||||
android:id="@+id/status_open_as"
|
||||
android:title="@string/action_open_as" />
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
<item
|
||||
android:id="@+id/status_open_as"
|
||||
android:title="@string/action_open_as" />
|
||||
<item
|
||||
android:id="@+id/status_open_in_web"
|
||||
android:title="@string/action_open_in_web" />
|
||||
<item
|
||||
android:id="@+id/status_mute_conversation"
|
||||
android:title="@string/action_mute_conversation"
|
||||
|
@ -40,7 +43,4 @@
|
|||
<item
|
||||
android:id="@+id/status_delete"
|
||||
android:title="@string/action_delete" />
|
||||
<item
|
||||
android:id="@+id/status_delete_and_redraft"
|
||||
android:title="@string/action_delete_and_redraft" />
|
||||
</menu>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_open_in_external_app"
|
||||
android:icon="@drawable/ic_exit_to_app_24px"
|
||||
android:title="@string/action_open_in_external_app"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/action_download"
|
||||
android:icon="@drawable/ic_file_download_black_24dp"
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/action_open_in_web"
|
||||
android:title="@string/action_open_in_web"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_reveal"
|
||||
android:title="@string/expand_collapse_all_statuses"
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<dimen name="card_image_horizontal_width">100dp</dimen>
|
||||
|
||||
<dimen name="compose_activity_snackbar_elevation">16dp</dimen>
|
||||
<dimen name="compose_activity_bottom_bar_height">60dp</dimen>
|
||||
|
||||
<dimen name="account_activity_scroll_title_visible_height">200dp</dimen>
|
||||
<dimen name="account_activity_avatar_size">100dp</dimen>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<string name="action_enable_formatting_syntax">Enable %s</string>
|
||||
<string name="action_disable_formatting_syntax">Disable %s</string>
|
||||
<string name="action_sticker">Stickers</string>
|
||||
<string name="action_open_in_external_app">Open in external app</string>
|
||||
|
||||
<string name="title_emoji_reacted_by">%s reacted by</string>
|
||||
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
<string name="action_mute">Mute</string>
|
||||
<string name="action_unmute">Unmute</string>
|
||||
<string name="action_mute_domain">Mute %s</string>
|
||||
<string name="action_unmute_domain">Unmute %s</string>
|
||||
<string name="action_mute_conversation">Mute conversation</string>
|
||||
<string name="action_unmute_conversation">Unmute conversation</string>
|
||||
<string name="action_mention">Mention</string>
|
||||
|
|
|
@ -19,6 +19,7 @@ package com.keylesspalace.tusky
|
|||
import android.text.SpannedString
|
||||
import android.widget.EditText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
||||
|
@ -143,6 +144,8 @@ class ComposeActivityTest {
|
|||
activity.accountManager = accountManagerMock
|
||||
activity.viewModelFactory = viewModelFactoryMock
|
||||
|
||||
activity.eventHub = EventHubImpl
|
||||
|
||||
controller.create().start()
|
||||
}
|
||||
|
||||
|
@ -503,6 +506,7 @@ class ComposeActivityTest {
|
|||
),
|
||||
maximumTootCharacters,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 275 KiB |
Loading…
Reference in New Issue