Add send/recv/render support for text stories.

This commit is contained in:
Alex Hart
2022-03-09 13:11:56 -04:00
committed by Cody Henthorne
parent 3a2e8b9b19
commit ff8d7fa6c2
40 changed files with 963 additions and 93 deletions

View File

@@ -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(

View File

@@ -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() {

View File

@@ -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())
}
}

View File

@@ -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()
}

View File

@@ -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.