Add new My Story privacy settings.
This commit is contained in:
parent
ebc556801e
commit
9bc25132c3
58 changed files with 935 additions and 242 deletions
|
@ -122,4 +122,62 @@ class SQLiteDatabaseTest {
|
|||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
|
||||
val hasRun = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
try {
|
||||
db.beginTransaction()
|
||||
hasRun.set(true)
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(hasRun.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun2.set(true)
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
hasRun1.set(true)
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android" android:enterFadeDuration="250" android:exitFadeDuration="250">
|
||||
<item android:state_checked="true" android:state_enabled="false">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_colorOutline" />
|
||||
<stroke android:width="1.5dp" android:color="@color/signal_colorOnPrimary" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_x_error_inverse_16" android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp"/>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_colorErrorContainerInverse" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_x_error_inverse_16" android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp"/>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_checked="false">
|
||||
<shape android:shape="oval">
|
||||
<stroke android:width="1.5dp" android:color="@color/signal_colorOutline" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
9
app/src/main/res/drawable/ic_x_error_inverse_16.xml
Normal file
9
app/src/main/res/drawable/ic_x_error_inverse_16.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="@color/signal_colorErrorInverse"
|
||||
android:pathData="M12.354,4.354l-0.708,-0.708l-3.646,3.647l-3.646,-3.647l-0.708,0.708l3.647,3.646l-3.647,3.646l0.708,0.708l3.646,-3.647l3.646,3.647l0.708,-0.708l-3.647,-3.646l3.647,-3.646z"/>
|
||||
</vector>
|
72
app/src/main/res/layout/dsl_learn_more_preference_item.xml
Normal file
72
app/src/main/res/layout/dsl_learn_more_preference_item.xml
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/dsl_preference_item_background"
|
||||
android:minHeight="56dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/ic_advanced_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||
app:layout_constraintEnd_toStartOf="@id/icon_end"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||
tools:text="Message font size" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:lineSpacingExtra="4sp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/text_color_secondary_enabled_selector"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/icon_end"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||
app:layout_goneMarginTop="16dp"
|
||||
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_end"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/ic_advanced_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -7,16 +7,16 @@
|
|||
android:background="@drawable/dsl_preference_item_background"
|
||||
android:minHeight="56dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
<RadioButton
|
||||
android:id="@+id/radio_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:importantForAccessibility="no"
|
||||
android:clickable="false"
|
||||
android:theme="@style/Signal.Widget.CompoundButton.RadioButton"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/ic_advanced_24" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
|
@ -27,8 +27,8 @@
|
|||
android:layout_marginEnd="24dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||
app:layout_constraintEnd_toStartOf="@id/radio_widget"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/icon"
|
||||
app:layout_constraintStart_toEndOf="@id/radio_widget"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
|
@ -46,23 +46,23 @@
|
|||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/radio_widget"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/icon"
|
||||
app:layout_constraintStart_toEndOf="@id/radio_widget"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||
app:layout_goneMarginTop="16dp"
|
||||
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:clickable="false"
|
||||
android:theme="@style/Signal.Widget.CompoundButton.RadioButton"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/ic_advanced_24" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -5,8 +5,12 @@
|
|||
android:id="@+id/my_story_settings"
|
||||
app:startDestination="@id/myStorySettings">
|
||||
<fragment
|
||||
android:id="@+id/hideStoryFromFragment"
|
||||
android:name="org.thoughtcrime.securesms.stories.settings.hide.HideStoryFromFragment"
|
||||
android:id="@+id/allExceptFragment"
|
||||
android:name="org.thoughtcrime.securesms.stories.settings.privacy.AllExceptFragment"
|
||||
android:label="hide_story_from_fragment" />
|
||||
<fragment
|
||||
android:id="@+id/onlyShareWithFragment"
|
||||
android:name="org.thoughtcrime.securesms.stories.settings.privacy.OnlyShareWithFragment"
|
||||
android:label="hide_story_from_fragment" />
|
||||
<fragment
|
||||
android:id="@+id/myStorySettings"
|
||||
|
@ -16,8 +20,11 @@
|
|||
android:id="@+id/action_myStorySettings_to_signalConnectionsBottomSheet"
|
||||
app:destination="@id/signalConnectionsBottomSheet" />
|
||||
<action
|
||||
android:id="@+id/action_myStorySettings_to_hideStoryFromFragment"
|
||||
app:destination="@id/hideStoryFromFragment" />
|
||||
android:id="@+id/action_myStorySettings_to_allExceptFragment"
|
||||
app:destination="@id/allExceptFragment" />
|
||||
<action
|
||||
android:id="@+id/action_myStorySettings_to_onlyShareWithFragment"
|
||||
app:destination="@id/onlyShareWithFragment" />
|
||||
</fragment>
|
||||
<dialog
|
||||
android:id="@+id/signalConnectionsBottomSheet"
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
<color name="signal_colorNeutralInverse">@color/signal_dark_colorNeutralInverse</color>
|
||||
<color name="signal_colorNeutralVariantInverse">@color/signal_dark_colorNeutralVariantInverse</color>
|
||||
|
||||
<!-- Inverses -->
|
||||
<color name="signal_colorErrorInverse">@color/signal_light_colorError</color>
|
||||
<color name="signal_colorErrorContainerInverse">@color/signal_light_colorErrorContainer</color>
|
||||
|
||||
<!-- Core color alpha variants -->
|
||||
<color name="signal_colorSecondaryContainer_12">#1F414659</color>
|
||||
<color name="signal_colorSurface_60">#991B1C1F</color>
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
<color name="signal_colorNeutralInverse">@color/signal_light_colorNeutralInverse</color>
|
||||
<color name="signal_colorNeutralVariantInverse">@color/signal_light_colorNeutralVariantInverse</color>
|
||||
|
||||
<!-- Inverses -->
|
||||
<color name="signal_colorErrorInverse">@color/signal_dark_colorError</color>
|
||||
<color name="signal_colorErrorContainerInverse">@color/signal_dark_colorErrorContainer</color>
|
||||
|
||||
<!-- Core color alpha variants -->
|
||||
<color name="signal_colorSecondaryContainer_12">#1FDCE5F9</color>
|
||||
<color name="signal_colorSurface_60">#99FBFCFF</color>
|
||||
|
|
|
@ -4638,11 +4638,30 @@
|
|||
<string name="MyStorySettingsFragment__who_can_see_this_story">Who can see this story</string>
|
||||
<!-- Clickable option for selecting people to hide your story from -->
|
||||
<string name="MyStorySettingsFragment__hide_story_from">Hide story from</string>
|
||||
<!-- Summary of clickable option displaying how many people you have hidden your story from -->
|
||||
<!-- Privacy setting title for sending stories to all your signal connections -->
|
||||
<string name="MyStorySettingsFragment__all_signal_connections">All Signal connections</string>
|
||||
<!-- Privacy setting description for sending stories to all your signal connections -->
|
||||
<string name="MyStorySettingsFragment__share_with_all_connections">Share with all connections</string>
|
||||
<!-- Privacy setting title for sending stories to all except the specified connections -->
|
||||
<string name="MyStorySettingsFragment__all_signal_connections_except">All Signal connections except…</string>
|
||||
<!-- Privacy setting description for sending stories to all except the specified connections -->
|
||||
<string name="MyStorySettingsFragment__hide_your_story_from_specific_people">Hide your story from specific people</string>
|
||||
<!-- Summary of clickable option displaying how many people you have excluded from your story -->
|
||||
<plurals name="MyStorySettingsFragment__d_people_excluded">
|
||||
<item quantity="one">%1$d person excluded</item>
|
||||
<item quantity="other">%1$d people excluded</item>
|
||||
</plurals>
|
||||
<!-- Privacy setting title for only sharing your story with specified connections -->
|
||||
<string name="MyStorySettingsFragment__only_share_with">Only share with…</string>
|
||||
<!-- Privacy setting description for only sharing your story with specified connections -->
|
||||
<string name="MyStorySettingsFragment__only_share_with_selected_people">Only share with selected people</string>
|
||||
<!-- Summary of clickable option displaying how many people you have included to send to in your story -->
|
||||
<plurals name="MyStorySettingsFragment__d_people">
|
||||
<item quantity="one">%1$d person</item>
|
||||
<item quantity="other">%1$d people</item>
|
||||
</plurals>
|
||||
<!-- My story privacy fine print about what the privacy settings are for -->
|
||||
<string name="MyStorySettingsFragment__choose_who_can_view_your_story">Choose who can view your story. Changes won\'t affect stories you\'ve already sent.</string>
|
||||
<!-- Section header for options related to replies and reactions -->
|
||||
<string name="MyStorySettingsFragment__replies_amp_reactions">Replies & reactions</string>
|
||||
<!-- Switchable option for allowing replies and reactions on your stories -->
|
||||
|
@ -4651,10 +4670,10 @@
|
|||
<string name="MyStorySettingsFragment__let_people_who_can_view_your_story_react_and_reply">Let people who can view your story react and reply</string>
|
||||
<!-- Note about default sharing -->
|
||||
<string name="MyStorySettingsFragment__hide_your_story_from">Hide your story from specific people. By default, your story is shared with your %1$s</string>
|
||||
<!-- Signal connections linked text that opens the Signal Connections sheet -->
|
||||
<string name="MyStorySettingsFragment__signal_connections">Signal connections.</string>
|
||||
<!-- Signal connections bolded text in the Signal Connections sheet -->
|
||||
<string name="SignalConnectionsBottomSheet___signal_connections">Signal Connections</string>
|
||||
<!-- Displayed at the top of the signal connections sheet. Please remember to insert strong tag as required. -->
|
||||
<string name="SignalConnectionsBottomSheet__signal_connections_are_people"><strong>Signal Connections</strong> are people you\'ve chosen to trust, either by:</string>
|
||||
<string name="SignalConnectionsBottomSheet__signal_connections_are_people">Signal Connections are people you\'ve chosen to trust, either by:</string>
|
||||
<!-- Signal connections sheet bullet point 1 -->
|
||||
<string name="SignalConnectionsBottomSheet__starting_a_conversation">Starting a conversation</string>
|
||||
<!-- Signal connections sheet bullet point 2 -->
|
||||
|
@ -4703,8 +4722,10 @@
|
|||
<string name="TextStoryPostSendFragment__search">Search</string>
|
||||
<!-- Toast shown when an unexpected error occurs while sending a text story -->
|
||||
<string name="TextStoryPostSendFragment__an_unexpected_error_occurred_try_again">An unexpected error occurred</string>
|
||||
<!-- Title for screen allowing user to hide "My Story" entries from specific people -->
|
||||
<string name="HideStoryFromFragment__hide_story_from">Hide story from…</string>
|
||||
<!-- Title for screen allowing user to exclude "My Story" entries from specific people -->
|
||||
<string name="ChangeMyStoryMembershipFragment__all_except">All except…</string>
|
||||
<!-- Title for screen allowing user to only share "My Story" entries with specific people -->
|
||||
<string name="ChangeMyStoryMembershipFragment__only_share_with">Only share with…</string>
|
||||
<!-- Done button label for hide story from screen -->
|
||||
<string name="HideStoryFromFragment__done">Done</string>
|
||||
<!-- Dialog title for first time adding something to a story -->
|
||||
|
|
|
@ -55,11 +55,22 @@ fun Cursor.isNull(column: String): Boolean {
|
|||
return CursorUtil.isNull(this, column)
|
||||
}
|
||||
|
||||
inline fun <T> Cursor.readToList(mapper: (Cursor) -> T): List<T> {
|
||||
fun <T> Cursor.requireObject(column: String, serializer: LongSerializer<T>): T {
|
||||
return serializer.deserialize(CursorUtil.requireLong(this, column))
|
||||
}
|
||||
|
||||
fun <T> Cursor.requireObject(column: String, serializer: StringSerializer<T>): T {
|
||||
return serializer.deserialize(CursorUtil.requireString(this, column))
|
||||
}
|
||||
|
||||
inline fun <T> Cursor.readToList(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): List<T> {
|
||||
val list = mutableListOf<T>()
|
||||
use {
|
||||
while (moveToNext()) {
|
||||
list += mapper(this)
|
||||
val record = mapper(this)
|
||||
if (predicate(record)) {
|
||||
list += mapper(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
|
|
|
@ -7,6 +7,23 @@ import androidx.core.content.contentValuesOf
|
|||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
|
||||
/**
|
||||
* Begins a transaction on the `this` database, runs the provided [block] providing the `this` value as it's argument
|
||||
* within the transaction, and then ends the transaction successfully.
|
||||
*
|
||||
* @return The value returned by [block] if any
|
||||
*/
|
||||
fun <T : SupportSQLiteDatabase, R> T.withinTransaction(block: (T) -> R): R {
|
||||
beginTransaction()
|
||||
try {
|
||||
val toReturn = block(this)
|
||||
setTransactionSuccessful()
|
||||
return toReturn
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
fun SupportSQLiteDatabase.getTableRowCount(table: String): Int {
|
||||
return this.query("SELECT COUNT(*) FROM $table").use {
|
||||
if (it.moveToFirst()) {
|
||||
|
@ -224,4 +241,3 @@ class DeleteBuilderPart2(
|
|||
return db.delete(tableName, where, whereArgs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
13
core-util/src/main/java/org/signal/core/util/Serializer.kt
Normal file
13
core-util/src/main/java/org/signal/core/util/Serializer.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package org.signal.core.util
|
||||
|
||||
/**
|
||||
* Generic serialization interface for use with database and store operations.
|
||||
*/
|
||||
interface Serializer<T, R> {
|
||||
fun serialize(data: T): R
|
||||
fun deserialize(data: R): T
|
||||
}
|
||||
|
||||
interface StringSerializer<T> : Serializer<T, String>
|
||||
|
||||
interface LongSerializer<T> : Serializer<T, Long>
|
|
@ -74,6 +74,10 @@ public class SignalStoryDistributionListRecord implements SignalRecord {
|
|||
return proto.getAllowsReplies();
|
||||
}
|
||||
|
||||
public boolean isBlockList() {
|
||||
return proto.getIsBlockList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describeDiff(SignalRecord other) {
|
||||
if (other instanceof SignalStoryDistributionListRecord) {
|
||||
|
@ -104,6 +108,10 @@ public class SignalStoryDistributionListRecord implements SignalRecord {
|
|||
diff.add("AllowsReplies");
|
||||
}
|
||||
|
||||
if (this.isBlockList() != that.isBlockList()) {
|
||||
diff.add("BlockList");
|
||||
}
|
||||
|
||||
return diff.toString();
|
||||
} else {
|
||||
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
|
||||
|
@ -166,6 +174,11 @@ public class SignalStoryDistributionListRecord implements SignalRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setIsBlockList(boolean isBlockList) {
|
||||
builder.setIsBlockList(isBlockList);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalStoryDistributionListRecord build() {
|
||||
return new SignalStoryDistributionListRecord(id, builder.build());
|
||||
}
|
||||
|
|
|
@ -165,4 +165,5 @@ message StoryDistributionListRecord {
|
|||
repeated string recipientUuids = 3;
|
||||
uint64 deletedAtTimestamp = 4;
|
||||
bool allowsReplies = 5;
|
||||
bool isBlockList = 6;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue