mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Add send/recv/render support for text stories.
This commit is contained in:
committed by
Cody Henthorne
parent
3a2e8b9b19
commit
ff8d7fa6c2
@@ -9,16 +9,16 @@ object TextStoryBackgroundColors {
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 191.41f,
|
||||
colors = intArrayOf(0xFFF53844.toInt(), 0xFFF33845.toInt(), 0xFFEC3848.toInt(), 0xFFE2384C.toInt(), 0xFFD63851.toInt(), 0xFFC73857.toInt(), 0xFFB6385E.toInt(), 0xFFA43866.toInt(), 0xFF93376D.toInt(), 0xFF813775.toInt(), 0xFF70377C.toInt(), 0xFF613782.toInt(), 0xFF553787.toInt(), 0xFF4B378B.toInt(), 0xFF44378E.toInt(), 0xFF42378F.toInt()),
|
||||
positions = floatArrayOf(0.2109f, 0.2168f, 0.2339f, 0.2611f, 0.2975f, 0.3418f, 0.3932f, 0.4506f, 0.5129f, 0.5791f, 0.6481f, 0.719f, 0.7907f, 0.8621f, 0.9322f, 1.0f)
|
||||
colors = intArrayOf(0xFFF53844.toInt(), 0xFF42378F.toInt()),
|
||||
positions = floatArrayOf(0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 192.04f,
|
||||
colors = intArrayOf(0xFFF04CE6.toInt(), 0xFFEE4BE6.toInt(), 0xFFE54AE5.toInt(), 0xFFD949E5.toInt(), 0xFFC946E4.toInt(), 0xFFB644E3.toInt(), 0xFFA141E3.toInt(), 0xFF8B3FE2.toInt(), 0xFF743CE1.toInt(), 0xFF5E39E0.toInt(), 0xFF4936DF.toInt(), 0xFF3634DE.toInt(), 0xFF2632DD.toInt(), 0xFF1930DD.toInt(), 0xFF112FDD.toInt(), 0xFF0E2FDD.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFFF04CE6.toInt(), 0xFF0E2FDD.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
),
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
@@ -33,16 +33,16 @@ object TextStoryBackgroundColors {
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 180f,
|
||||
colors = intArrayOf(0xFF0093E9.toInt(), 0xFF0294E9.toInt(), 0xFF0696E7.toInt(), 0xFF0D99E5.toInt(), 0xFF169EE3.toInt(), 0xFF21A3E0.toInt(), 0xFF2DA8DD.toInt(), 0xFF3AAEDA.toInt(), 0xFF46B5D6.toInt(), 0xFF53BBD3.toInt(), 0xFF5FC0D0.toInt(), 0xFF6AC5CD.toInt(), 0xFF73CACB.toInt(), 0xFF7ACDC9.toInt(), 0xFF7ECFC7.toInt(), 0xFF80D0C7.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFF0093E9.toInt(), 0xFF80D0C7.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forGradient(
|
||||
id = ChatColors.Id.NotSet,
|
||||
linearGradient = ChatColors.LinearGradient(
|
||||
degrees = 180f,
|
||||
colors = intArrayOf(0xFF65CDAC.toInt(), 0xFF64CDAB.toInt(), 0xFF60CBA8.toInt(), 0xFF5BC8A3.toInt(), 0xFF55C49D.toInt(), 0xFF4DC096.toInt(), 0xFF45BB8F.toInt(), 0xFF3CB687.toInt(), 0xFF33B17F.toInt(), 0xFF2AAC76.toInt(), 0xFF21A76F.toInt(), 0xFF1AA268.toInt(), 0xFF139F62.toInt(), 0xFF0E9C5E.toInt(), 0xFF0B9A5B.toInt(), 0xFF0A995A.toInt()),
|
||||
positions = floatArrayOf(0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1.0f)
|
||||
colors = intArrayOf(0xFF65CDAC.toInt(), 0xFF0A995A.toInt()),
|
||||
positions = floatArrayOf(0.0f, 1.0f)
|
||||
)
|
||||
),
|
||||
ChatColors.forColor(
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
@@ -45,7 +46,7 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
||||
|
||||
private val viewModel: TextStoryPostSendViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
TextStoryPostSendViewModel.Factory(TextStoryPostSendRepository())
|
||||
TextStoryPostSendViewModel.Factory(TextStoryPostSendRepository(requireContext()))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -99,6 +100,10 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
||||
}
|
||||
}
|
||||
|
||||
disposables += viewModel.untrustedIdentities.subscribe {
|
||||
SafetyNumberChangeDialog.show(childFragmentManager, it)
|
||||
}
|
||||
|
||||
searchField.doAfterTextChanged {
|
||||
contactSearchMediator.onFilterChanged(it?.toString())
|
||||
}
|
||||
@@ -158,9 +163,8 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
|
||||
shareConfirmButton.isEnabled = false
|
||||
|
||||
val textStoryPostCreationState = creationViewModel.state.value
|
||||
val linkPreviewState = linkPreviewViewModel.linkPreviewState.value
|
||||
|
||||
viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewState!!)
|
||||
viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewViewModel.onSend().firstOrNull())
|
||||
}
|
||||
|
||||
private fun animateInSelection() {
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.RecipientSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
class TextStoryPostSendRepository {
|
||||
class TextStoryPostSendRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun isFirstSendToStory(shareContacts: Set<ContactSearchKey>): Boolean {
|
||||
if (SignalStore.storyValues().userHasAddedToAStory) {
|
||||
@@ -16,8 +34,92 @@ class TextStoryPostSendRepository {
|
||||
return shareContacts.any { it is ContactSearchKey.Story }
|
||||
}
|
||||
|
||||
fun send(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Completable {
|
||||
// TODO [stories] -- Implementation once we know what text post messages look like.
|
||||
return Completable.complete()
|
||||
fun send(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
|
||||
return checkForBadIdentityRecords(contactSearchKey).flatMap { result ->
|
||||
if (result is TextStoryPostSendResult.Success) {
|
||||
performSend(contactSearchKey, textStoryPostCreationState, linkPreview)
|
||||
} else {
|
||||
Single.just(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForBadIdentityRecords(contactSearchKeys: Set<ContactSearchKey>): Single<TextStoryPostSendResult> {
|
||||
return Single.fromCallable {
|
||||
val recipients: List<Recipient> = contactSearchKeys
|
||||
.filterIsInstance<RecipientSearchKey>()
|
||||
.map { Recipient.resolved(it.recipientId) }
|
||||
val identityRecordList: IdentityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
|
||||
|
||||
if (identityRecordList.untrustedRecords.isNotEmpty()) {
|
||||
TextStoryPostSendResult.UntrustedRecordsError(identityRecordList.untrustedRecords)
|
||||
} else {
|
||||
TextStoryPostSendResult.Success
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun performSend(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
|
||||
return Single.fromCallable {
|
||||
val messages: MutableList<OutgoingSecureMediaMessage> = mutableListOf()
|
||||
|
||||
for (contact in contactSearchKey) {
|
||||
val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get())
|
||||
val isStory = contact is ContactSearchKey.Story || recipient.isDistributionList
|
||||
|
||||
if (isStory && recipient.isActiveGroup) {
|
||||
SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId())
|
||||
}
|
||||
|
||||
val storyType: StoryType = when {
|
||||
recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId())
|
||||
isStory -> StoryType.STORY_WITH_REPLIES
|
||||
else -> StoryType.NONE
|
||||
}
|
||||
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
serializeTextStoryState(textStoryPostCreationState),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType.toTextStoryType(),
|
||||
null,
|
||||
null,
|
||||
emptyList(),
|
||||
listOfNotNull(linkPreview),
|
||||
emptyList(),
|
||||
mutableSetOf(),
|
||||
mutableSetOf()
|
||||
)
|
||||
|
||||
messages.add(OutgoingSecureMediaMessage(message))
|
||||
ThreadUtil.sleep(5)
|
||||
}
|
||||
|
||||
MessageSender.sendMediaBroadcast(context, messages, emptyList())
|
||||
TextStoryPostSendResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
private fun serializeTextStoryState(textStoryPostCreationState: TextStoryPostCreationState): String {
|
||||
val builder = StoryTextPost.newBuilder()
|
||||
|
||||
builder.body = textStoryPostCreationState.body.toString()
|
||||
builder.background = textStoryPostCreationState.backgroundColor.serialize()
|
||||
builder.style = when (textStoryPostCreationState.textFont) {
|
||||
TextFont.REGULAR -> StoryTextPost.Style.REGULAR
|
||||
TextFont.BOLD -> StoryTextPost.Style.BOLD
|
||||
TextFont.SERIF -> StoryTextPost.Style.SERIF
|
||||
TextFont.SCRIPT -> StoryTextPost.Style.SCRIPT
|
||||
TextFont.CONDENSED -> StoryTextPost.Style.CONDENSED
|
||||
}
|
||||
builder.textBackgroundColor = textStoryPostCreationState.textBackgroundColor
|
||||
builder.textForegroundColor = textStoryPostCreationState.textForegroundColor
|
||||
|
||||
return Base64.encodeBytes(builder.build().toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
|
||||
sealed class TextStoryPostSendResult {
|
||||
object Success : TextStoryPostSendResult()
|
||||
data class UntrustedRecordsError(val untrustedRecords: List<IdentityRecord>) : TextStoryPostSendResult()
|
||||
}
|
||||
@@ -3,20 +3,25 @@ package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class TextStoryPostSendViewModel(private val repository: TextStoryPostSendRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(TextStoryPostSendState.INIT)
|
||||
private val untrustedIdentitySubject = PublishSubject.create<List<IdentityRecord>>()
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<TextStoryPostSendState> = store.stateLiveData
|
||||
val untrustedIdentities: Observable<List<IdentityRecord>> = untrustedIdentitySubject
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
@@ -36,14 +41,22 @@ class TextStoryPostSendViewModel(private val repository: TextStoryPostSendReposi
|
||||
}
|
||||
}
|
||||
|
||||
fun onSend(contactSearchKeys: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreviewState: LinkPreviewViewModel.LinkPreviewState) {
|
||||
fun onSend(contactSearchKeys: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?) {
|
||||
store.update {
|
||||
TextStoryPostSendState.SENDING
|
||||
}
|
||||
|
||||
disposables += repository.send(contactSearchKeys, textStoryPostCreationState, linkPreviewState.linkPreview.orNull()).subscribeBy(
|
||||
onComplete = {
|
||||
store.update { TextStoryPostSendState.SENT }
|
||||
disposables += repository.send(contactSearchKeys, textStoryPostCreationState, linkPreview).subscribeBy(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is TextStoryPostSendResult.Success -> {
|
||||
store.update { TextStoryPostSendState.SENT }
|
||||
}
|
||||
is TextStoryPostSendResult.UntrustedRecordsError -> {
|
||||
untrustedIdentitySubject.onNext(it.untrustedRecords)
|
||||
store.update { TextStoryPostSendState.INIT }
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
// TODO [stories] -- Error of some sort.
|
||||
|
||||
Reference in New Issue
Block a user