mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-18 15:56:24 +01:00
Add new My Story privacy settings.
This commit is contained in:
@@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
@@ -385,7 +386,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti,
|
||||
currentSelection);
|
||||
currentSelection,
|
||||
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class DSLSettingsAdapter : MappingAdapter() {
|
||||
@@ -29,6 +30,7 @@ class DSLSettingsAdapter : MappingAdapter() {
|
||||
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(LongClickPreference::class.java, LayoutFactory(::LongClickPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(TextPreference::class.java, LayoutFactory(::TextPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(LearnMoreTextPreference::class.java, LayoutFactory(::LearnMoreTextPreferenceViewHolder, R.layout.dsl_learn_more_preference_item))
|
||||
registerFactory(RadioListPreference::class.java, LayoutFactory(::RadioListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(MultiSelectListPreference::class.java, LayoutFactory(::MultiSelectListPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
registerFactory(ExternalLinkPreference::class.java, LayoutFactory(::ExternalLinkPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
@@ -91,6 +93,14 @@ abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : Ma
|
||||
|
||||
class TextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<TextPreference>(itemView)
|
||||
|
||||
class LearnMoreTextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<LearnMoreTextPreference>(itemView) {
|
||||
override fun bind(model: LearnMoreTextPreference) {
|
||||
super.bind(model)
|
||||
(titleView as LearnMoreTextView).setOnLinkClickListener { model.onClick() }
|
||||
(summaryView as LearnMoreTextView).setOnLinkClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPreference>(itemView) {
|
||||
override fun bind(model: ClickPreference) {
|
||||
super.bind(model)
|
||||
|
||||
@@ -185,6 +185,15 @@ class DSLConfiguration {
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun learnMoreTextPref(
|
||||
title: DSLSettingsText? = null,
|
||||
summary: DSLSettingsText? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = LearnMoreTextPreference(title, summary, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun toMappingModelList(): MappingModelList = MappingModelList().apply { addAll(children) }
|
||||
}
|
||||
|
||||
@@ -218,6 +227,12 @@ class TextPreference(
|
||||
summary: DSLSettingsText?
|
||||
) : PreferenceModel<TextPreference>(title = title, summary = summary)
|
||||
|
||||
class LearnMoreTextPreference(
|
||||
override val title: DSLSettingsText?,
|
||||
override val summary: DSLSettingsText?,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<LearnMoreTextPreference>()
|
||||
|
||||
class DividerPreference : PreferenceModel<DividerPreference>() {
|
||||
override fun areItemsTheSame(newItem: DividerPreference) = true
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
private final int checkboxResource;
|
||||
|
||||
private final SelectedContactSet selectedContacts = new SelectedContactSet();
|
||||
|
||||
@@ -205,14 +206,16 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
boolean multiSelect,
|
||||
@NonNull Set<RecipientId> currentContacts)
|
||||
@NonNull Set<RecipientId> currentContacts,
|
||||
int checkboxResource)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
this.checkboxResource = checkboxResource;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -229,7 +232,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_CONTACT) {
|
||||
return new ContactViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
View view = layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false);
|
||||
view.findViewById(R.id.check_box).setBackgroundResource(checkboxResource);
|
||||
return new ContactViewHolder(view, clickListener);
|
||||
} else {
|
||||
return new DividerViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.contacts.selection
|
||||
|
||||
import android.os.Bundle
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -15,7 +16,8 @@ data class ContactSelectionArguments(
|
||||
val canSelectSelf: Boolean = selectionLimits == null,
|
||||
val displayChips: Boolean = true,
|
||||
val recyclerPadBottom: Int = -1,
|
||||
val recyclerChildClipping: Boolean = true
|
||||
val recyclerChildClipping: Boolean = true,
|
||||
val checkboxResource: Int = R.drawable.contact_selection_checkbox
|
||||
) {
|
||||
|
||||
fun toArgumentBundle(): Bundle {
|
||||
@@ -29,6 +31,7 @@ data class ContactSelectionArguments(
|
||||
putBoolean(DISPLAY_CHIPS, displayChips)
|
||||
putInt(RV_PADDING_BOTTOM, recyclerPadBottom)
|
||||
putBoolean(RV_CLIP, recyclerChildClipping)
|
||||
putInt(CHECKBOX_RESOURCE, checkboxResource)
|
||||
putParcelableArrayList(CURRENT_SELECTION, ArrayList(currentSelection))
|
||||
}
|
||||
}
|
||||
@@ -44,5 +47,6 @@ data class ContactSelectionArguments(
|
||||
const val DISPLAY_CHIPS = "display_chips"
|
||||
const val RV_PADDING_BOTTOM = "recycler_view_padding_bottom"
|
||||
const val RV_CLIP = "recycler_view_clipping"
|
||||
const val CHECKBOX_RESOURCE = "checkbox_resource"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import org.thoughtcrime.securesms.stories.Stories.getHeaderAction
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.hide.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.privacy.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
|
||||
@@ -6,13 +6,19 @@ import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireObject
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -35,6 +41,9 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
@JvmField
|
||||
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||
|
||||
@JvmField
|
||||
val CREATE_INDEXES: Array<String> = arrayOf(MembershipTable.CREATE_INDEX)
|
||||
|
||||
const val RECIPIENT_ID = ListTable.RECIPIENT_ID
|
||||
const val DISTRIBUTION_ID = ListTable.DISTRIBUTION_ID
|
||||
const val LIST_TABLE_NAME = ListTable.TABLE_NAME
|
||||
@@ -55,7 +64,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
ListTable.ID to DistributionListId.MY_STORY_ID,
|
||||
ListTable.NAME to DistributionId.MY_STORY.toString(),
|
||||
ListTable.DISTRIBUTION_ID to DistributionId.MY_STORY.toString(),
|
||||
ListTable.RECIPIENT_ID to recipientId
|
||||
ListTable.RECIPIENT_ID to recipientId,
|
||||
ListTable.PRIVACY_MODE to DistributionListPrivacyMode.ALL.serialize()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -71,8 +81,9 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
const val IS_UNKNOWN = "is_unknown"
|
||||
const val PRIVACY_MODE = "privacy_mode"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$NAME TEXT UNIQUE NOT NULL,
|
||||
@@ -80,11 +91,14 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
$ALLOWS_REPLIES INTEGER DEFAULT 1,
|
||||
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
|
||||
$IS_UNKNOWN INTEGER DEFAULT 0
|
||||
$IS_UNKNOWN INTEGER DEFAULT 0,
|
||||
$PRIVACY_MODE INTEGER DEFAULT ${DistributionListPrivacyMode.ONLY_WITH.serialize()}
|
||||
)
|
||||
"""
|
||||
|
||||
const val IS_NOT_DELETED = "$DELETION_TIMESTAMP == 0"
|
||||
|
||||
val LIST_UI_PROJECTION = arrayOf(ID, NAME, RECIPIENT_ID, ALLOWS_REPLIES, IS_UNKNOWN, PRIVACY_MODE)
|
||||
}
|
||||
|
||||
private object MembershipTable {
|
||||
@@ -93,15 +107,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
const val ID = "_id"
|
||||
const val LIST_ID = "list_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val PRIVACY_MODE = "privacy_mode"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$LIST_ID INTEGER NOT NULL REFERENCES ${ListTable.TABLE_NAME} (${ListTable.ID}) ON DELETE CASCADE,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
UNIQUE($LIST_ID, $RECIPIENT_ID) ON CONFLICT IGNORE
|
||||
$PRIVACY_MODE INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
const val CREATE_INDEX = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON $TABLE_NAME ($LIST_ID, $RECIPIENT_ID, $PRIVACY_MODE)"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,28 +136,13 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
) == 1
|
||||
}
|
||||
|
||||
fun getAllListsForContactSelectionUi(query: String?, includeMyStory: Boolean): List<DistributionListPartialRecord> {
|
||||
return getAllListsForContactSelectionUiCursor(query, includeMyStory)?.use {
|
||||
val results = mutableListOf<DistributionListPartialRecord>()
|
||||
while (it.moveToNext()) {
|
||||
results.add(
|
||||
DistributionListPartialRecord(
|
||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
results
|
||||
} ?: emptyList()
|
||||
fun setPrivacyMode(distributionListId: DistributionListId, privacyMode: DistributionListPrivacyMode) {
|
||||
val values = contentValuesOf(ListTable.PRIVACY_MODE to privacyMode.serialize())
|
||||
writableDatabase.update(ListTable.TABLE_NAME, values, "${ListTable.ID} = ?", SqlUtil.buildArgs(distributionListId))
|
||||
}
|
||||
|
||||
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
|
||||
val db = readableDatabase
|
||||
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||
|
||||
val where = when {
|
||||
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
|
||||
@@ -155,24 +157,32 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
else -> SqlUtil.buildArgs(SqlUtil.buildCaseInsensitiveGlobPattern(query), DistributionListId.MY_STORY_ID)
|
||||
}
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, where, whereArgs, null, null, null)
|
||||
return db.query(ListTable.TABLE_NAME, ListTable.LIST_UI_PROJECTION, where, whereArgs, null, null, null)
|
||||
}
|
||||
|
||||
fun getAllListRecipients(): List<RecipientId> {
|
||||
return readableDatabase
|
||||
.select(ListTable.RECIPIENT_ID)
|
||||
.from(ListTable.TABLE_NAME)
|
||||
.run()
|
||||
.readToList { cursor -> RecipientId.from(cursor.requireLong(ListTable.RECIPIENT_ID)) }
|
||||
}
|
||||
|
||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||
val db = readableDatabase
|
||||
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
|
||||
return db.query(ListTable.TABLE_NAME, ListTable.LIST_UI_PROJECTION, selection, null, null, null, null)?.use { cursor ->
|
||||
val results = mutableListOf<DistributionListPartialRecord>()
|
||||
while (it.moveToNext()) {
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(
|
||||
DistributionListPartialRecord(
|
||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||
id = DistributionListId.from(CursorUtil.requireLong(cursor, ListTable.ID)),
|
||||
name = CursorUtil.requireString(cursor, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -235,7 +245,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
allowsReplies: Boolean = true,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: ByteArray? = null,
|
||||
isUnknown: Boolean = false
|
||||
isUnknown: Boolean = false,
|
||||
privacyMode: DistributionListPrivacyMode = DistributionListPrivacyMode.ONLY_WITH
|
||||
): DistributionListId? {
|
||||
val db = writableDatabase
|
||||
|
||||
@@ -248,6 +259,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
putNull(ListTable.RECIPIENT_ID)
|
||||
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||
put(ListTable.IS_UNKNOWN, isUnknown)
|
||||
put(ListTable.PRIVACY_MODE, privacyMode.serialize())
|
||||
}
|
||||
|
||||
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
||||
@@ -264,7 +276,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
SqlUtil.buildArgs(id)
|
||||
)
|
||||
|
||||
members.forEach { addMemberToList(DistributionListId.from(id), it) }
|
||||
members.forEach { addMemberToList(DistributionListId.from(id), privacyMode, it) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
@@ -311,15 +323,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
|
||||
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -331,15 +346,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
|
||||
val privacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getRawMembers(id),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = emptyList(),
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -358,28 +376,36 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
|
||||
fun getMembers(listId: DistributionListId): List<RecipientId> {
|
||||
if (listId == DistributionListId.MY_STORY) {
|
||||
val blockedMembers = getRawMembers(listId).toSet()
|
||||
lateinit var privacyMode: DistributionListPrivacyMode
|
||||
lateinit var rawMembers: List<RecipientId>
|
||||
|
||||
return SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val result = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
val id = RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID))
|
||||
if (!blockedMembers.contains(id)) {
|
||||
result.add(id)
|
||||
}
|
||||
}
|
||||
result
|
||||
} ?: emptyList()
|
||||
} else {
|
||||
return getRawMembers(listId)
|
||||
readableDatabase.withinTransaction {
|
||||
privacyMode = getPrivacyMode(listId)
|
||||
rawMembers = getRawMembers(listId, privacyMode)
|
||||
}
|
||||
|
||||
return when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> {
|
||||
SignalDatabase.recipients
|
||||
.getSignalContacts(false)!!
|
||||
.readToList { it.requireObject(RecipientDatabase.ID, RecipientId.SERIALIZER) }
|
||||
}
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> {
|
||||
SignalDatabase.recipients
|
||||
.getSignalContacts(false)!!
|
||||
.readToList(
|
||||
predicate = { !rawMembers.contains(it) },
|
||||
mapper = { it.requireObject(RecipientDatabase.ID, RecipientId.SERIALIZER) }
|
||||
)
|
||||
}
|
||||
DistributionListPrivacyMode.ONLY_WITH -> rawMembers
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawMembers(listId: DistributionListId): List<RecipientId> {
|
||||
fun getRawMembers(listId: DistributionListId, privacyMode: DistributionListPrivacyMode): List<RecipientId> {
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, null, "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, null, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, privacyMode.serialize()), null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
members.add(RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID)))
|
||||
}
|
||||
@@ -389,15 +415,35 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
|
||||
fun getMemberCount(listId: DistributionListId): Int {
|
||||
return if (listId == DistributionListId.MY_STORY) {
|
||||
SignalDatabase.recipients.getSignalContacts(false)?.count?.let { it - getRawMemberCount(listId) } ?: 0
|
||||
} else {
|
||||
getRawMemberCount(listId)
|
||||
}
|
||||
return getPrivacyData(listId).memberCount
|
||||
}
|
||||
|
||||
fun getRawMemberCount(listId: DistributionListId): Int {
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, SqlUtil.buildArgs("COUNT(*)"), "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
fun getPrivacyData(listId: DistributionListId): DistributionListPrivacyData {
|
||||
lateinit var privacyMode: DistributionListPrivacyMode
|
||||
var rawMemberCount = 0
|
||||
var totalContactCount = 0
|
||||
|
||||
readableDatabase.withinTransaction {
|
||||
privacyMode = getPrivacyMode(listId)
|
||||
rawMemberCount = getRawMemberCount(listId, privacyMode)
|
||||
totalContactCount = SignalDatabase.recipients.getSignalContactsCount(false)
|
||||
}
|
||||
|
||||
val memberCount = when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> totalContactCount
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> totalContactCount - rawMemberCount
|
||||
DistributionListPrivacyMode.ONLY_WITH -> rawMemberCount
|
||||
}
|
||||
|
||||
return DistributionListPrivacyData(
|
||||
privacyMode = privacyMode,
|
||||
rawMemberCount = rawMemberCount,
|
||||
memberCount = memberCount
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRawMemberCount(listId: DistributionListId, privacyMode: DistributionListPrivacyMode): Int {
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, SqlUtil.buildArgs("COUNT(*)"), "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, privacyMode.serialize()), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
cursor.getInt(0)
|
||||
} else {
|
||||
@@ -406,24 +452,46 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberFromList(listId: DistributionListId, member: RecipientId) {
|
||||
writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(listId, member))
|
||||
private fun getPrivacyMode(listId: DistributionListId): DistributionListPrivacyMode {
|
||||
return readableDatabase
|
||||
.select(ListTable.PRIVACY_MODE)
|
||||
.from(ListTable.TABLE_NAME)
|
||||
.where("${ListTable.ID} = ?", listId.serialize())
|
||||
.run()
|
||||
.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
} else {
|
||||
DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addMemberToList(listId: DistributionListId, member: RecipientId) {
|
||||
fun removeMemberFromList(listId: DistributionListId, privacyMode: DistributionListPrivacyMode, member: RecipientId) {
|
||||
writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, member, privacyMode.serialize()))
|
||||
}
|
||||
|
||||
fun addMemberToList(listId: DistributionListId, privacyMode: DistributionListPrivacyMode, member: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.LIST_ID, listId.serialize())
|
||||
put(MembershipTable.RECIPIENT_ID, member.serialize())
|
||||
put(MembershipTable.PRIVACY_MODE, privacyMode.serialize())
|
||||
}
|
||||
|
||||
writableDatabase.insert(MembershipTable.TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
fun removeAllMembers(listId: DistributionListId) {
|
||||
writableDatabase
|
||||
.delete(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.LIST_ID} = ?", listId.serialize())
|
||||
.run()
|
||||
}
|
||||
|
||||
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.RECIPIENT_ID, newId.serialize())
|
||||
}
|
||||
|
||||
writableDatabase.update(MembershipTable.TABLE_NAME, values, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(oldId))
|
||||
}
|
||||
|
||||
@@ -487,12 +555,19 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
throw AssertionError("Should never try to insert My Story")
|
||||
}
|
||||
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
insert.isBlockList && insert.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
insert.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
createList(
|
||||
name = insert.name,
|
||||
members = insert.recipients.map(RecipientId::from),
|
||||
distributionId = distributionId,
|
||||
allowsReplies = insert.allowsReplies(),
|
||||
deletionTimestamp = insert.deletedAtTimestamp,
|
||||
privacyMode = privacyMode,
|
||||
storageId = insert.id.raw
|
||||
)
|
||||
}
|
||||
@@ -526,12 +601,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
return
|
||||
}
|
||||
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
update.new.isBlockList && update.new.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
update.new.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val listTableValues = contentValuesOf(
|
||||
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
|
||||
ListTable.NAME to update.new.name,
|
||||
ListTable.IS_UNKNOWN to false
|
||||
ListTable.IS_UNKNOWN to false,
|
||||
ListTable.PRIVACY_MODE to privacyMode.serialize()
|
||||
)
|
||||
|
||||
writableDatabase.update(
|
||||
@@ -541,22 +622,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
SqlUtil.buildArgs(distributionId.toString())
|
||||
)
|
||||
|
||||
val currentlyInDistributionList = getRawMembers(distributionListId).toSet()
|
||||
val currentlyInDistributionList = getRawMembers(distributionListId, privacyMode).toSet()
|
||||
val shouldBeInDistributionList = update.new.recipients.map(RecipientId::from).toSet()
|
||||
val toRemove = currentlyInDistributionList - shouldBeInDistributionList
|
||||
val toAdd = shouldBeInDistributionList - currentlyInDistributionList
|
||||
|
||||
toRemove.forEach {
|
||||
removeMemberFromList(distributionListId, it)
|
||||
removeMemberFromList(distributionListId, privacyMode, it)
|
||||
}
|
||||
|
||||
toAdd.forEach {
|
||||
addMemberToList(distributionListId, it)
|
||||
addMemberToList(distributionListId, privacyMode, it)
|
||||
}
|
||||
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
writableDatabase.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.InvalidKeyException
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
@@ -821,6 +822,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
fun markNeedsSync(recipientIds: Collection<RecipientId>) {
|
||||
writableDatabase
|
||||
.withinTransaction {
|
||||
for (recipientId in recipientIds) {
|
||||
markNeedsSync(recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markNeedsSync(recipientId: RecipientId) {
|
||||
rotateStorageId(recipientId)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
|
||||
@@ -2301,6 +2311,14 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
fun getSignalContacts(includeSelf: Boolean): Cursor? {
|
||||
return getSignalContacts(includeSelf, "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE")
|
||||
}
|
||||
|
||||
fun getSignalContactsCount(includeSelf: Boolean): Int {
|
||||
return getSignalContacts(includeSelf)?.count ?: 0
|
||||
}
|
||||
|
||||
fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? {
|
||||
val searchSelection = ContactSearchSelection.Builder()
|
||||
.withRegistered(true)
|
||||
.withGroups(false)
|
||||
@@ -2308,7 +2326,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.build()
|
||||
val selection = searchSelection.where
|
||||
val args = searchSelection.args
|
||||
val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE"
|
||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,14 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
private final net.zetetic.database.sqlcipher.SQLiteDatabase wrapped;
|
||||
private final Tracer tracer;
|
||||
|
||||
private static final ThreadLocal<Set<Runnable>> POST_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Set<Runnable>> PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS;
|
||||
private static final ThreadLocal<Set<Runnable>> POST_SUCCESSFUL_TRANSACTION_TASKS;
|
||||
|
||||
static {
|
||||
POST_TRANSACTION_TASKS.set(new LinkedHashSet<>());
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
POST_SUCCESSFUL_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.set(new LinkedHashSet<>());
|
||||
}
|
||||
|
||||
public SQLiteDatabase(net.zetetic.database.sqlcipher.SQLiteDatabase wrapped) {
|
||||
@@ -125,7 +130,7 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
*/
|
||||
public void runPostSuccessfulTransaction(@NonNull Runnable task) {
|
||||
if (wrapped.inTransaction()) {
|
||||
getPostTransactionTasks().add(task);
|
||||
getPendingPostSuccessfulTransactionTasks().add(task);
|
||||
} else {
|
||||
task.run();
|
||||
}
|
||||
@@ -137,18 +142,29 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
*/
|
||||
public void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable task) {
|
||||
if (wrapped.inTransaction()) {
|
||||
getPostTransactionTasks().add(new DedupedRunnable(dedupeKey, task));
|
||||
getPendingPostSuccessfulTransactionTasks().add(new DedupedRunnable(dedupeKey, task));
|
||||
} else {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Set<Runnable> getPostTransactionTasks() {
|
||||
Set<Runnable> tasks = POST_TRANSACTION_TASKS.get();
|
||||
private @NonNull Set<Runnable> getPendingPostSuccessfulTransactionTasks() {
|
||||
Set<Runnable> tasks = PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.get();
|
||||
|
||||
if (tasks == null) {
|
||||
tasks = new LinkedHashSet<>();
|
||||
POST_TRANSACTION_TASKS.set(tasks);
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.set(tasks);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private @NonNull Set<Runnable> getPostSuccessfulTransactionTasks() {
|
||||
Set<Runnable> tasks = POST_SUCCESSFUL_TRANSACTION_TASKS.get();
|
||||
|
||||
if (tasks == null) {
|
||||
tasks = new LinkedHashSet<>();
|
||||
POST_SUCCESSFUL_TRANSACTION_TASKS.set(tasks);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
@@ -278,16 +294,16 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
|
||||
@Override
|
||||
public void onCommit() {
|
||||
Set<Runnable> tasks = getPostTransactionTasks();
|
||||
for (Runnable r : new HashSet<>(tasks)) {
|
||||
r.run();
|
||||
}
|
||||
Set<Runnable> pendingTasks = getPendingPostSuccessfulTransactionTasks();
|
||||
Set<Runnable> tasks = getPostSuccessfulTransactionTasks();
|
||||
tasks.clear();
|
||||
tasks.addAll(pendingTasks);
|
||||
pendingTasks.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRollback() {
|
||||
getPostTransactionTasks().clear();
|
||||
getPendingPostSuccessfulTransactionTasks().clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -297,6 +313,12 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
public void endTransaction() {
|
||||
trace("endTransaction()", wrapped::endTransaction);
|
||||
traceLockEnd();
|
||||
|
||||
Set<Runnable> tasks = getPostSuccessfulTransactionTasks();
|
||||
for (Runnable r : new HashSet<>(tasks)) {
|
||||
r.run();
|
||||
}
|
||||
tasks.clear();
|
||||
}
|
||||
|
||||
public void setTransactionSuccessful() {
|
||||
|
||||
@@ -131,6 +131,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS)
|
||||
db.execSQL(StorySendsDatabase.CREATE_INDEX)
|
||||
executeStatements(db, DistributionListDatabase.CREATE_INDEXES)
|
||||
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||
|
||||
@@ -201,8 +201,9 @@ object SignalDatabaseMigrations {
|
||||
private const val GROUP_STORY_REPLY_CLEANUP = 145
|
||||
private const val REMOTE_MEGAPHONE = 146
|
||||
private const val QUOTE_INDEX = 147
|
||||
private const val MY_STORY_PRIVACY_MODE = 148
|
||||
|
||||
const val DATABASE_VERSION = 147
|
||||
const val DATABASE_VERSION = 148
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2622,6 +2623,41 @@ object SignalDatabaseMigrations {
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
if (oldVersion < MY_STORY_PRIVACY_MODE) {
|
||||
db.execSQL("ALTER TABLE distribution_list ADD COLUMN privacy_mode INTEGER DEFAULT 0")
|
||||
db.execSQL("UPDATE distribution_list SET privacy_mode = 1 WHERE _id = 1")
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE distribution_list_member_tmp (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id),
|
||||
privacy_mode INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO distribution_list_member_tmp
|
||||
SELECT
|
||||
_id,
|
||||
list_id,
|
||||
recipient_id,
|
||||
0
|
||||
FROM distribution_list_member
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL("DROP TABLE distribution_list_member")
|
||||
db.execSQL("ALTER TABLE distribution_list_member_tmp RENAME TO distribution_list_member")
|
||||
|
||||
db.execSQL("UPDATE distribution_list_member SET privacy_mode = 1 WHERE list_id = 1")
|
||||
|
||||
db.execSQL("CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class DistributionListId implements DatabaseId, Parcelable {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public boolean isMyStory() {
|
||||
return equals(MY_STORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(id);
|
||||
|
||||
@@ -7,5 +7,6 @@ data class DistributionListPartialRecord(
|
||||
val name: CharSequence,
|
||||
val recipientId: RecipientId,
|
||||
val allowsReplies: Boolean,
|
||||
val isUnknown: Boolean
|
||||
val isUnknown: Boolean,
|
||||
val privacyMode: DistributionListPrivacyMode
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
/**
|
||||
* Data needed to know how a distribution privacy settings are configured.
|
||||
*/
|
||||
data class DistributionListPrivacyData(
|
||||
val privacyMode: DistributionListPrivacyMode,
|
||||
val rawMemberCount: Int,
|
||||
val memberCount: Int
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* A list can explicit ([ONLY_WITH]) where only members of the list can send or exclusionary ([ALL_EXCEPT]) where
|
||||
* all connections are sent the story except for those members of the list. [ALL] is all of your Signal Connections.
|
||||
*/
|
||||
enum class DistributionListPrivacyMode(private val code: Long) {
|
||||
ONLY_WITH(0),
|
||||
ALL_EXCEPT(1),
|
||||
ALL(2);
|
||||
|
||||
val isBlockList: Boolean
|
||||
get() = this != ONLY_WITH
|
||||
|
||||
fun serialize(): Long {
|
||||
return code
|
||||
}
|
||||
|
||||
companion object Serializer : LongSerializer<DistributionListPrivacyMode> {
|
||||
override fun serialize(data: DistributionListPrivacyMode): Long {
|
||||
return data.serialize()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): DistributionListPrivacyMode {
|
||||
return when (data) {
|
||||
ONLY_WITH.code -> ONLY_WITH
|
||||
ALL_EXCEPT.code -> ALL_EXCEPT
|
||||
ALL.code -> ALL
|
||||
else -> throw AssertionError("Unknown privacy mode: $data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,16 @@ data class DistributionListRecord(
|
||||
val name: String,
|
||||
val distributionId: DistributionId,
|
||||
val allowsReplies: Boolean,
|
||||
val rawMembers: List<RecipientId>,
|
||||
val members: List<RecipientId>,
|
||||
val deletedAtTimestamp: Long,
|
||||
val isUnknown: Boolean
|
||||
)
|
||||
val isUnknown: Boolean,
|
||||
val privacyMode: DistributionListPrivacyMode
|
||||
) {
|
||||
fun getMembersToSync(): List<RecipientId> {
|
||||
return when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> emptyList()
|
||||
else -> rawMembers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.migrations.EmojiDownloadMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.KbsEnclaveMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
|
||||
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.PinOptOutMigration;
|
||||
import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob;
|
||||
@@ -206,6 +207,7 @@ public final class JobManagerFactories {
|
||||
put(KbsEnclaveMigrationJob.KEY, new KbsEnclaveMigrationJob.Factory());
|
||||
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
|
||||
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
|
||||
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
||||
put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory());
|
||||
put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory());
|
||||
put(PniAccountInitializationMigrationJob.KEY, new PniAccountInitializationMigrationJob.Factory());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal fun SignalStoreValues.longValue(key: String, default: Long): SignalStoreValueDelegate<Long> {
|
||||
@@ -26,6 +27,10 @@ internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): Signa
|
||||
return BlobValue(key, default, this.store)
|
||||
}
|
||||
|
||||
internal fun <T : Any?> SignalStoreValues.enumValue(key: String, default: T, serializer: LongSerializer<T>): SignalStoreValueDelegate<T> {
|
||||
return KeyValueEnumValue(key, default, serializer, this.store)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin delegate that serves as a base for all other value types. This allows us to only expose this sealed
|
||||
* class to callers and protect the individual implementations as private behind the various extension functions.
|
||||
@@ -102,3 +107,17 @@ private class BlobValue(private val key: String, private val default: ByteArray,
|
||||
values.beginWrite().putBlob(key, value).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyValueEnumValue<T>(private val key: String, private val default: T, private val serializer: LongSerializer<T>, store: KeyValueStore) : SignalStoreValueDelegate<T>(store) {
|
||||
override fun getValue(values: KeyValueStore): T {
|
||||
return if (values.containsKey(key)) {
|
||||
serializer.deserialize(values.getLong(key, 0))
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(values: KeyValueStore, value: T) {
|
||||
values.beginWrite().putLong(key, serializer.serialize(value)).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.core.util.StringSerializer;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SignalStoreList;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -50,7 +51,7 @@ abstract class SignalStoreValues {
|
||||
return store.getBlob(key, defaultValue);
|
||||
}
|
||||
|
||||
<T> List<T> getList(@NonNull String key, @NonNull Serializer<T> serializer) {
|
||||
<T> List<T> getList(@NonNull String key, @NonNull StringSerializer<T> serializer) {
|
||||
byte[] blob = getBlob(key, null);
|
||||
if (blob == null) {
|
||||
return Collections.emptyList();
|
||||
@@ -93,7 +94,7 @@ abstract class SignalStoreValues {
|
||||
store.beginWrite().putString(key, value).apply();
|
||||
}
|
||||
|
||||
<T> void putList(@NonNull String key, @NonNull List<T> values, @NonNull Serializer<T> serializer) {
|
||||
<T> void putList(@NonNull String key, @NonNull List<T> values, @NonNull StringSerializer<T> serializer) {
|
||||
putBlob(key, SignalStoreList.newBuilder()
|
||||
.addAllContents(values.stream()
|
||||
.map(serializer::serialize)
|
||||
@@ -105,9 +106,4 @@ abstract class SignalStoreValues {
|
||||
void remove(@NonNull String key) {
|
||||
store.beginWrite().remove(key).apply();
|
||||
}
|
||||
|
||||
interface Serializer<T> {
|
||||
@NonNull String serialize(@NonNull T data);
|
||||
T deserialize(@NonNull String data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.signal.core.util.StringSerializer
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
|
||||
@@ -62,7 +63,7 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
return storySends.filter { it.timestamp >= activeCutoffTimestamp }
|
||||
}
|
||||
|
||||
private object StorySendSerializer : Serializer<StorySend> {
|
||||
private object StorySendSerializer : StringSerializer<StorySend> {
|
||||
|
||||
override fun serialize(data: StorySend): String {
|
||||
return JSONObject()
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendReposi
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendResult
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostView
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
import org.thoughtcrime.securesms.stories.settings.hide.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.privacy.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.hide.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.privacy.HideStoryFromDialogFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
@@ -102,9 +102,10 @@ public class ApplicationMigrations {
|
||||
static final int PNI_IDENTITY_3 = 58;
|
||||
static final int STORY_DISTRIBUTION_LIST_SYNC = 59;
|
||||
static final int EMOJI_VERSION_7 = 60;
|
||||
static final int MY_STORY_PRIVACY_MODE = 61;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 60;
|
||||
public static final int CURRENT_VERSION = 61;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -446,6 +447,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.EMOJI_VERSION_7, new EmojiDownloadMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.MY_STORY_PRIVACY_MODE) {
|
||||
jobs.put(Version.MY_STORY_PRIVACY_MODE, new SyncDistributionListsMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
|
||||
/**
|
||||
* Marks all distribution lists as needing to be synced with storage service.
|
||||
*/
|
||||
public final class SyncDistributionListsMigrationJob extends MigrationJob {
|
||||
|
||||
public static final String KEY = "SyncDistributionListsMigrationJob";
|
||||
|
||||
SyncDistributionListsMigrationJob() {
|
||||
this(new Parameters.Builder().build());
|
||||
}
|
||||
|
||||
private SyncDistributionListsMigrationJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUiBlocking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performMigration() {
|
||||
SignalDatabase.recipients().markNeedsSync(SignalDatabase.distributionLists().getAllListRecipients());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean shouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<SyncDistributionListsMigrationJob> {
|
||||
@Override
|
||||
public @NonNull SyncDistributionListsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new SyncDistributionListsMigrationJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.DatabaseId;
|
||||
import org.signal.core.util.LongSerializer;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
@@ -29,6 +30,7 @@ public class RecipientId implements Parcelable, Comparable<RecipientId>, Databas
|
||||
private static final char DELIMITER = ',';
|
||||
|
||||
public static final RecipientId UNKNOWN = RecipientId.from(UNKNOWN_ID);
|
||||
public static final LongSerializer<RecipientId> SERIALIZER = new Serializer();
|
||||
|
||||
private final long id;
|
||||
|
||||
@@ -212,4 +214,16 @@ public class RecipientId implements Parcelable, Comparable<RecipientId>, Databas
|
||||
|
||||
private static class InvalidLongRecipientIdError extends AssertionError {}
|
||||
private static class InvalidStringRecipientIdError extends AssertionError {}
|
||||
|
||||
private static class Serializer implements LongSerializer<RecipientId> {
|
||||
@Override
|
||||
public Long serialize(RecipientId data) {
|
||||
return data.toLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RecipientId deserialize(Long data) {
|
||||
return RecipientId.from(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -193,13 +194,15 @@ public final class StorageSyncModels {
|
||||
return new SignalStoryDistributionListRecord.Builder(rawStorageId, recipient.getSyncExtras().getStorageProto())
|
||||
.setIdentifier(UuidUtil.toByteArray(record.getDistributionId().asUuid()))
|
||||
.setName(record.getName())
|
||||
.setRecipients(record.getMembers().stream()
|
||||
.setRecipients(record.getMembersToSync()
|
||||
.stream()
|
||||
.map(Recipient::resolved)
|
||||
.filter(Recipient::hasServiceId)
|
||||
.map(Recipient::requireServiceId)
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList()))
|
||||
.setAllowsReplies(record.getAllowsReplies())
|
||||
.setIsBlockList(record.getPrivacyMode().isBlockList())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,10 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr
|
||||
List<SignalServiceAddress> recipients = remote.getRecipients();
|
||||
long deletedAtTimestamp = remote.getDeletedAtTimestamp();
|
||||
boolean allowsReplies = remote.allowsReplies();
|
||||
boolean isBlockList = remote.isBlockList();
|
||||
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies);
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies, isBlockList);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies, isBlockList);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
@@ -104,6 +105,7 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr
|
||||
.setRecipients(recipients)
|
||||
.setDeletedAtTimestamp(deletedAtTimestamp)
|
||||
.setAllowsReplies(allowsReplies)
|
||||
.setIsBlockList(isBlockList)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -131,14 +133,16 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr
|
||||
@Nullable byte[] unknownFields,
|
||||
@Nullable byte[] identifier,
|
||||
@Nullable String name,
|
||||
@NonNull List<SignalServiceAddress> recipients,
|
||||
@NonNull List<SignalServiceAddress> recipients,
|
||||
long deletedAtTimestamp,
|
||||
boolean allowsReplies) {
|
||||
boolean allowsReplies,
|
||||
boolean isBlockList) {
|
||||
return Arrays.equals(unknownFields, record.serializeUnknownFields()) &&
|
||||
Arrays.equals(identifier, record.getIdentifier()) &&
|
||||
Objects.equals(name, record.getName()) &&
|
||||
Objects.equals(recipients, record.getRecipients()) &&
|
||||
deletedAtTimestamp == record.getDeletedAtTimestamp() &&
|
||||
allowsReplies == record.allowsReplies();
|
||||
Arrays.equals(identifier, record.getIdentifier()) &&
|
||||
Objects.equals(name, record.getName()) &&
|
||||
Objects.equals(recipients, record.getRecipients()) &&
|
||||
deletedAtTimestamp == record.getDeletedAtTimestamp() &&
|
||||
allowsReplies == record.allowsReplies() &&
|
||||
isBlockList == record.isBlockList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
@@ -33,7 +34,7 @@ object StoryDialogs {
|
||||
.setPositiveButton(R.string.StoryDialogs__add_to_story) { _, _ ->
|
||||
onAddToStory.invoke()
|
||||
}
|
||||
.setNeutralButton(R.string.StoryDialogs__edit_viewers) { _, _ -> onEditViewers.invoke() }
|
||||
.setNeutralButton(R.string.StoryDialogs__edit_viewers) { _, _ -> Toast.makeText(context, "New flow coming soon", Toast.LENGTH_SHORT).show() }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> onCancel.invoke() }
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
|
||||
@@ -16,10 +16,10 @@ class PrivateStorySettingsRepository {
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun removeMember(distributionListId: DistributionListId, member: RecipientId): Completable {
|
||||
fun removeMember(distributionListRecord: DistributionListRecord, member: RecipientId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListId, member)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListRecord.id, distributionListRecord.privacyMode, member)
|
||||
Stories.onStorySettingsChanged(distributionListRecord.id)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class PrivateStorySettingsViewModel(private val distributionListId: Distribution
|
||||
}
|
||||
|
||||
fun remove(recipient: Recipient) {
|
||||
disposables += repository.removeMember(distributionListId, recipient.id)
|
||||
disposables += repository.removeMember(store.state.privateStory!!, recipient.id)
|
||||
.subscribe {
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.hide
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment
|
||||
|
||||
/**
|
||||
* Allows user to select a list of people to exclude from "My Story"
|
||||
*/
|
||||
class HideStoryFromFragment : BaseStoryRecipientSelectionFragment() {
|
||||
override val actionButtonLabel: Int = R.string.HideStoryFromFragment__done
|
||||
|
||||
override val distributionListId: DistributionListId
|
||||
get() = DistributionListId.from(DistributionListId.MY_STORY_ID)
|
||||
|
||||
override val toolbarTitleId: Int = R.string.HideStoryFromFragment__hide_story_from
|
||||
|
||||
override fun presentTitle(toolbar: Toolbar, size: Int) = Unit
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.my
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
|
||||
data class MyStoryPrivacyState(val privacyMode: DistributionListPrivacyMode? = null, val connectionCount: Int = 0)
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.my
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -9,28 +10,22 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class MyStorySettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.MyStorySettingsFragment__my_story
|
||||
) {
|
||||
|
||||
private val viewModel: MyStorySettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
MyStorySettingsViewModel.Factory(MyStorySettingsRepository())
|
||||
}
|
||||
)
|
||||
private val viewModel: MyStorySettingsViewModel by viewModels()
|
||||
|
||||
private val signalConnectionsSummary by lazy {
|
||||
SpanUtil.clickSubstring(
|
||||
getString(R.string.MyStorySettingsFragment__hide_your_story_from, getString(R.string.MyStorySettingsFragment__signal_connections)),
|
||||
getString(R.string.MyStorySettingsFragment__signal_connections),
|
||||
{
|
||||
findNavController().safeNavigate(R.id.action_myStorySettings_to_signalConnectionsBottomSheet)
|
||||
},
|
||||
ContextCompat.getColor(requireContext(), R.color.signal_text_primary)
|
||||
)
|
||||
private lateinit var lifecycleDisposable: LifecycleDisposable
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycleDisposable = LifecycleDisposable()
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -48,16 +43,57 @@ class MyStorySettingsFragment : DSLSettingsFragment(
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.MyStorySettingsFragment__who_can_see_this_story)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.MyStorySettingsFragment__hide_story_from),
|
||||
summary = DSLSettingsText.from(resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people, state.hiddenStoryFromCount, state.hiddenStoryFromCount)),
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.MyStorySettingsFragment__all_signal_connections),
|
||||
summary = DSLSettingsText.from(R.string.MyStorySettingsFragment__share_with_all_connections),
|
||||
isChecked = state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL,
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_myStorySettings_to_hideStoryFromFragment)
|
||||
lifecycleDisposable += viewModel.setMyStoryPrivacyMode(DistributionListPrivacyMode.ALL)
|
||||
.subscribe()
|
||||
}
|
||||
)
|
||||
|
||||
val exceptText = if (state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) {
|
||||
DSLSettingsText.from(resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people_excluded, state.myStoryPrivacyState.connectionCount, state.myStoryPrivacyState.connectionCount))
|
||||
} else {
|
||||
DSLSettingsText.from(R.string.MyStorySettingsFragment__hide_your_story_from_specific_people)
|
||||
}
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.MyStorySettingsFragment__all_signal_connections_except),
|
||||
summary = exceptText,
|
||||
isChecked = state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.setMyStoryPrivacyMode(DistributionListPrivacyMode.ALL_EXCEPT)
|
||||
.subscribe { findNavController().safeNavigate(R.id.action_myStorySettings_to_allExceptFragment) }
|
||||
}
|
||||
)
|
||||
|
||||
val onlyWithText = if (state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ONLY_WITH) {
|
||||
DSLSettingsText.from(resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people, state.myStoryPrivacyState.connectionCount, state.myStoryPrivacyState.connectionCount))
|
||||
} else {
|
||||
DSLSettingsText.from(R.string.MyStorySettingsFragment__only_share_with_selected_people)
|
||||
}
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.MyStorySettingsFragment__only_share_with),
|
||||
summary = onlyWithText,
|
||||
isChecked = state.myStoryPrivacyState.privacyMode == DistributionListPrivacyMode.ONLY_WITH,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.setMyStoryPrivacyMode(DistributionListPrivacyMode.ONLY_WITH)
|
||||
.subscribe { findNavController().safeNavigate(R.id.action_myStorySettings_to_onlyShareWithFragment) }
|
||||
}
|
||||
)
|
||||
|
||||
learnMoreTextPref(
|
||||
summary = DSLSettingsText.from(R.string.MyStorySettingsFragment__choose_who_can_view_your_story),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_myStorySettings_to_signalConnectionsBottomSheet)
|
||||
}
|
||||
)
|
||||
|
||||
textPref(summary = DSLSettingsText.from(signalConnectionsSummary))
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.MyStorySettingsFragment__replies_amp_reactions)
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.MyStorySettingsFragment__allow_replies_amp_reactions),
|
||||
|
||||
@@ -5,13 +5,27 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class MyStorySettingsRepository {
|
||||
|
||||
fun getHiddenRecipientCount(): Single<Int> {
|
||||
fun getPrivacyState(): Single<MyStoryPrivacyState> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.distributionLists.getRawMemberCount(DistributionListId.MY_STORY)
|
||||
val privacyData: DistributionListPrivacyData = SignalDatabase.distributionLists.getPrivacyData(DistributionListId.MY_STORY)
|
||||
|
||||
MyStoryPrivacyState(
|
||||
privacyMode = privacyData.privacyMode,
|
||||
connectionCount = if (privacyData.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) privacyData.rawMemberCount else privacyData.memberCount
|
||||
)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun setPrivacyMode(privacyMode: DistributionListPrivacyMode): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.setPrivacyMode(DistributionListId.MY_STORY, privacyMode)
|
||||
Stories.onStorySettingsChanged(DistributionListId.MY_STORY)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.my
|
||||
|
||||
data class MyStorySettingsState(
|
||||
val hiddenStoryFromCount: Int = 0,
|
||||
val myStoryPrivacyState: MyStoryPrivacyState = MyStoryPrivacyState(),
|
||||
val areRepliesAndReactionsEnabled: Boolean = false
|
||||
)
|
||||
|
||||
@@ -2,13 +2,14 @@ package org.thoughtcrime.securesms.stories.settings.my
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class MyStorySettingsViewModel(private val repository: MyStorySettingsRepository) : ViewModel() {
|
||||
class MyStorySettingsViewModel @JvmOverloads constructor(private val repository: MyStorySettingsRepository = MyStorySettingsRepository()) : ViewModel() {
|
||||
private val store = Store(MyStorySettingsState())
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
@@ -20,8 +21,8 @@ class MyStorySettingsViewModel(private val repository: MyStorySettingsRepository
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
disposables += repository.getHiddenRecipientCount()
|
||||
.subscribe { count -> store.update { it.copy(hiddenStoryFromCount = count) } }
|
||||
disposables += repository.getPrivacyState()
|
||||
.subscribe { myStoryPrivacyState -> store.update { it.copy(myStoryPrivacyState = myStoryPrivacyState) } }
|
||||
disposables += repository.getRepliesAndReactionsEnabled()
|
||||
.subscribe { repliesAndReactionsEnabled -> store.update { it.copy(areRepliesAndReactionsEnabled = repliesAndReactionsEnabled) } }
|
||||
}
|
||||
@@ -32,9 +33,13 @@ class MyStorySettingsViewModel(private val repository: MyStorySettingsRepository
|
||||
.subscribe { refresh() }
|
||||
}
|
||||
|
||||
class Factory(private val repository: MyStorySettingsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(MyStorySettingsViewModel(repository)) as T
|
||||
fun setMyStoryPrivacyMode(privacyMode: DistributionListPrivacyMode): Completable {
|
||||
return if (privacyMode == state.value!!.myStoryPrivacyState.privacyMode) {
|
||||
Completable.complete()
|
||||
} else {
|
||||
repository.setPrivacyMode(privacyMode)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnComplete { refresh() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
||||
class SignalConnectionsBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.stories_signal_connection_bottom_sheet, container, false)
|
||||
val view = inflater.inflate(R.layout.stories_signal_connection_bottom_sheet, container, false)
|
||||
view.findViewById<TextView>(R.id.text_1).text = SpanUtil.boldSubstring(getString(R.string.SignalConnectionsBottomSheet__signal_connections_are_people), getString(R.string.SignalConnectionsBottomSheet___signal_connections))
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.privacy
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment
|
||||
|
||||
abstract class ChangeMyStoryMembershipFragment : BaseStoryRecipientSelectionFragment() {
|
||||
override val actionButtonLabel: Int = R.string.HideStoryFromFragment__done
|
||||
|
||||
override val distributionListId: DistributionListId
|
||||
get() = DistributionListId.from(DistributionListId.MY_STORY_ID)
|
||||
|
||||
override fun presentTitle(toolbar: Toolbar, size: Int) = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows user to select a list of people to exclude from "My Story"
|
||||
*/
|
||||
class AllExceptFragment : ChangeMyStoryMembershipFragment() {
|
||||
override val toolbarTitleId: Int = R.string.ChangeMyStoryMembershipFragment__all_except
|
||||
override val checkboxResource: Int = R.drawable.contact_selection_exclude_checkbox
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows user to select a list of people to include for "My Story"
|
||||
*/
|
||||
class OnlyShareWithFragment : ChangeMyStoryMembershipFragment() {
|
||||
override val toolbarTitleId: Int = R.string.ChangeMyStoryMembershipFragment__only_share_with
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.hide
|
||||
package org.thoughtcrime.securesms.stories.settings.privacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -16,11 +16,7 @@ class HideStoryFromDialogFragment : DialogFragment(R.layout.fragment_container),
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, HideStoryFromFragment())
|
||||
.commit()
|
||||
}
|
||||
// TODO [stories] replace with new bottom sheet
|
||||
}
|
||||
|
||||
override fun exitFlow() {
|
||||
@@ -36,6 +36,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
||||
protected open val toolbarTitleId: Int = R.string.CreateStoryViewerSelectionFragment__choose_viewers
|
||||
abstract val actionButtonLabel: Int
|
||||
abstract val distributionListId: DistributionListId?
|
||||
protected open val checkboxResource: Int = R.drawable.contact_selection_checkbox
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var searchField: EditText
|
||||
@@ -75,8 +76,10 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
getAttachedContactSelectionFragment().markSelected(it.map(::ShareContact).toSet())
|
||||
presentTitle(toolbar, it.size)
|
||||
if (it.distributionListId == null || it.privateStory != null) {
|
||||
getAttachedContactSelectionFragment().markSelected(it.selection.map(::ShareContact).toSet())
|
||||
presentTitle(toolbar, it.selection.size)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.actionObservable.subscribe { action ->
|
||||
@@ -144,7 +147,8 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b
|
||||
canSelectSelf = false,
|
||||
currentSelection = emptyList(),
|
||||
displaySelectionCount = false,
|
||||
displayChips = true
|
||||
displayChips = true,
|
||||
checkboxResource = checkboxResource
|
||||
)
|
||||
|
||||
contactSelectionListFragment.arguments = arguments.toArgumentBundle()
|
||||
|
||||
@@ -7,34 +7,36 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class BaseStoryRecipientSelectionRepository {
|
||||
fun updateDistributionListMembership(distributionListId: DistributionListId, recipients: Set<RecipientId>) {
|
||||
|
||||
fun getRecord(distributionListId: DistributionListId): Single<DistributionListRecord> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.distributionLists.getList(distributionListId) ?: error("Record does not exist.")
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun updateDistributionListMembership(distributionListRecord: DistributionListRecord, recipients: Set<RecipientId>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val currentRecipients = SignalDatabase.distributionLists.getRawMembers(distributionListId).toSet()
|
||||
val currentRecipients = SignalDatabase.distributionLists.getRawMembers(distributionListRecord.id, distributionListRecord.privacyMode).toSet()
|
||||
val oldNotNew = currentRecipients - recipients
|
||||
val newNotOld = recipients - currentRecipients
|
||||
|
||||
oldNotNew.forEach {
|
||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListId, it)
|
||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListRecord.id, distributionListRecord.privacyMode, it)
|
||||
}
|
||||
|
||||
newNotOld.forEach {
|
||||
SignalDatabase.distributionLists.addMemberToList(distributionListId, it)
|
||||
SignalDatabase.distributionLists.addMemberToList(distributionListRecord.id, distributionListRecord.privacyMode, it)
|
||||
}
|
||||
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
Stories.onStorySettingsChanged(distributionListRecord.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getListMembers(distributionListId: DistributionListId): Single<Set<RecipientId>> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.distributionLists.getRawMembers(distributionListId).toSet()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getAllSignalContacts(): Single<Set<RecipientId>> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.stories.settings.select
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class BaseStoryRecipientSelectionState(
|
||||
val distributionListId: DistributionListId?,
|
||||
val privateStory: DistributionListRecord? = null,
|
||||
val selection: Set<RecipientId> = emptySet()
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
@@ -16,18 +17,19 @@ class BaseStoryRecipientSelectionViewModel(
|
||||
private val distributionListId: DistributionListId?,
|
||||
private val repository: BaseStoryRecipientSelectionRepository
|
||||
) : ViewModel() {
|
||||
private val store = Store(emptySet<RecipientId>())
|
||||
private val store = Store(BaseStoryRecipientSelectionState(distributionListId))
|
||||
private val subject = PublishSubject.create<Action>()
|
||||
private val disposable = CompositeDisposable()
|
||||
|
||||
var actionObservable: Observable<Action> = subject
|
||||
var state: LiveData<Set<RecipientId>> = store.stateLiveData
|
||||
var state: LiveData<BaseStoryRecipientSelectionState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
if (distributionListId != null) {
|
||||
disposable += repository.getListMembers(distributionListId)
|
||||
.subscribe { members ->
|
||||
store.update { it + members }
|
||||
disposable += repository.getRecord(distributionListId)
|
||||
.subscribe { record ->
|
||||
val startingSelection = if (record.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) record.rawMembers else record.members
|
||||
store.update { it.copy(privateStory = record, selection = it.selection + startingSelection) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,24 +40,24 @@ class BaseStoryRecipientSelectionViewModel(
|
||||
|
||||
fun toggleSelectAll() {
|
||||
disposable += repository.getAllSignalContacts().subscribeBy { allSignalRecipients ->
|
||||
store.update { allSignalRecipients }
|
||||
store.update { it.copy(selection = allSignalRecipients) }
|
||||
}
|
||||
}
|
||||
|
||||
fun addRecipient(recipientId: RecipientId) {
|
||||
store.update { it + recipientId }
|
||||
store.update { it.copy(selection = it.selection + recipientId) }
|
||||
}
|
||||
|
||||
fun removeRecipient(recipientId: RecipientId) {
|
||||
store.update { it - recipientId }
|
||||
store.update { it.copy(selection = it.selection - recipientId) }
|
||||
}
|
||||
|
||||
fun onAction() {
|
||||
if (distributionListId != null) {
|
||||
repository.updateDistributionListMembership(distributionListId, store.state)
|
||||
repository.updateDistributionListMembership(store.state.privateStory!!, store.state.selection)
|
||||
subject.onNext(Action.ExitFlow)
|
||||
} else {
|
||||
subject.onNext(Action.GoToNextScreen(store.state))
|
||||
subject.onNext(Action.GoToNextScreen(store.state.selection))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
@@ -42,7 +43,7 @@ public class LearnMoreTextView extends AppCompatTextView {
|
||||
private void init() {
|
||||
setMovementMethod(LinkMovementMethod.getInstance());
|
||||
setLinkTextInternal(R.string.LearnMoreTextView_learn_more);
|
||||
setLinkColor(ThemeUtil.getThemedColor(getContext(), R.attr.colorAccent));
|
||||
setLinkColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
|
||||
visible = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user