mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
Merge branch 'dev' into on
This commit is contained in:
commit
8ef8107101
@ -31,8 +31,8 @@ configurations.all {
|
|||||||
exclude module: "commons-logging"
|
exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 369
|
def canonicalVersionCode = 371
|
||||||
def canonicalVersionName = "1.18.1"
|
def canonicalVersionName = "1.18.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -41,6 +41,17 @@ def abiPostFix = ['armeabi-v7a' : 1,
|
|||||||
'x86_64' : 4,
|
'x86_64' : 4,
|
||||||
'universal' : 5]
|
'universal' : 5]
|
||||||
|
|
||||||
|
// Function to get the current git commit hash so we can embed it along w/ the build version.
|
||||||
|
// Note: This is visible in the SettingsActivity, right at the bottom (R.id.versionTextView).
|
||||||
|
def getGitHash = { ->
|
||||||
|
def stdout = new ByteArrayOutputStream()
|
||||||
|
exec {
|
||||||
|
commandLine "git", "rev-parse", "--short", "HEAD"
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
return stdout.toString().trim()
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion androidCompileSdkVersion
|
compileSdkVersion androidCompileSdkVersion
|
||||||
namespace 'network.loki.messenger'
|
namespace 'network.loki.messenger'
|
||||||
@ -94,6 +105,7 @@ android {
|
|||||||
project.ext.set("archivesBaseName", "session")
|
project.ext.set("archivesBaseName", "session")
|
||||||
|
|
||||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||||
|
buildConfigField "String", "GIT_HASH", "\"$getGitHash\""
|
||||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
@ -39,15 +39,20 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
|||||||
public int getDesiredTheme() {
|
public int getDesiredTheme() {
|
||||||
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
|
||||||
int userSelectedTheme = themeState.getTheme();
|
int userSelectedTheme = themeState.getTheme();
|
||||||
|
|
||||||
|
// If the user has configured Session to follow the system light/dark theme mode then do so..
|
||||||
if (themeState.getFollowSystem()) {
|
if (themeState.getFollowSystem()) {
|
||||||
// do light or dark based on the selected theme
|
|
||||||
|
// Use light or dark versions of the user's theme based on light-mode / dark-mode settings
|
||||||
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
|
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
|
||||||
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
|
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
|
||||||
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
|
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
|
||||||
} else {
|
} else {
|
||||||
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
|
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else // ..otherwise just return their selected theme.
|
||||||
|
{
|
||||||
return userSelectedTheme;
|
return userSelectedTheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.LinearLayout.VERTICAL
|
import android.widget.LinearLayout.VERTICAL
|
||||||
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
@ -16,7 +17,6 @@ import androidx.annotation.StyleRes
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.view.setMargins
|
import androidx.core.view.setMargins
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
@ -31,13 +31,16 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
private val dp20 = toPx(20, context.resources)
|
private val dp20 = toPx(20, context.resources)
|
||||||
private val dp40 = toPx(40, context.resources)
|
private val dp40 = toPx(40, context.resources)
|
||||||
|
private val dp60 = toPx(60, context.resources)
|
||||||
|
|
||||||
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||||
|
|
||||||
private var dialog: AlertDialog? = null
|
private var dialog: AlertDialog? = null
|
||||||
private fun dismiss() = dialog?.dismiss()
|
private fun dismiss() = dialog?.dismiss()
|
||||||
|
|
||||||
private val topView = LinearLayout(context).apply { orientation = VERTICAL }
|
private val topView = LinearLayout(context)
|
||||||
|
.apply { setPadding(0, dp20, 0, 0) }
|
||||||
|
.apply { orientation = VERTICAL }
|
||||||
.also(dialogBuilder::setCustomTitle)
|
.also(dialogBuilder::setCustomTitle)
|
||||||
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
private val buttonLayout = LinearLayout(context)
|
private val buttonLayout = LinearLayout(context)
|
||||||
@ -53,14 +56,14 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
fun title(text: CharSequence?) = title(text?.toString())
|
fun title(text: CharSequence?) = title(text?.toString())
|
||||||
fun title(text: String?) {
|
fun title(text: String?) {
|
||||||
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
|
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
||||||
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
||||||
text(text, style) {
|
text(text, style) {
|
||||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
.apply { updateMargins(dp40, 0, dp40, dp20) }
|
.apply { updateMargins(dp40, 0, dp40, 0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +75,10 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
modify()
|
modify()
|
||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
|
|
||||||
|
Space(context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, dp20)
|
||||||
|
}.let(topView::addView)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) }
|
fun htmlText(@StringRes id: Int, @StyleRes style: Int = 0, modify: TextView.() -> Unit = {}) { text(context.resources.getText(id)) }
|
||||||
@ -126,8 +133,7 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
) = Button(context, null, 0, style).apply {
|
) = Button(context, null, 0, style).apply {
|
||||||
setText(text)
|
setText(text)
|
||||||
contentDescription = resources.getString(contentDescriptionRes)
|
contentDescription = resources.getString(contentDescriptionRes)
|
||||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
|
||||||
.apply { setMargins(toPx(20, resources)) }
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
listener.invoke()
|
listener.invoke()
|
||||||
if (dismiss) dismiss()
|
if (dismiss) dismiss()
|
||||||
|
@ -93,6 +93,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (intent?.action == ACTION_ANSWER) {
|
if (intent?.action == ACTION_ANSWER) {
|
||||||
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
val answerIntent = WebRtcCallService.acceptCallIntent(this)
|
||||||
|
answerIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
ContextCompat.startForegroundService(this, answerIntent)
|
ContextCompat.startForegroundService(this, answerIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +107,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
setShowWhenLocked(true)
|
setShowWhenLocked(true)
|
||||||
setTurnScreenOn(true)
|
setTurnScreenOn(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addFlags(
|
window.addFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||||
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
@ -84,7 +85,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
|||||||
context.theme.resolveAttribute(item.iconRes, typedValue, true)
|
context.theme.resolveAttribute(item.iconRes, typedValue, true)
|
||||||
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
|
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
|
||||||
|
|
||||||
icon.imageTintList = color?.let(ColorStateList::valueOf)
|
icon.imageTintList = ColorStateList.valueOf(color ?: context.getColorFromAttr(android.R.attr.textColor))
|
||||||
}
|
}
|
||||||
item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
|
item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
|
||||||
title.setText(item.title)
|
title.setText(item.title)
|
||||||
|
@ -68,7 +68,7 @@ enum class ExpiryType(
|
|||||||
AFTER_SEND(
|
AFTER_SEND(
|
||||||
ExpiryMode::AfterSend,
|
ExpiryMode::AfterSend,
|
||||||
R.string.expiration_type_disappear_after_send,
|
R.string.expiration_type_disappear_after_send,
|
||||||
R.string.expiration_type_disappear_after_read_description,
|
R.string.expiration_type_disappear_after_send_description,
|
||||||
R.string.AccessibilityId_disappear_after_send_option
|
R.string.AccessibilityId_disappear_after_send_option
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -35,11 +35,28 @@ class ContactListAdapter(
|
|||||||
binding.profilePictureView.update(contact.recipient)
|
binding.profilePictureView.update(contact.recipient)
|
||||||
binding.nameTextView.text = contact.displayName
|
binding.nameTextView.text = contact.displayName
|
||||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
binding.root.setOnClickListener { listener(contact.recipient) }
|
||||||
|
|
||||||
|
// TODO: When we implement deleting contacts (hide might be safest for now) then probably set a long-click listener here w/ something like:
|
||||||
|
/*
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
Log.w("[ACL]", "Long clicked on contact ${contact.recipient.name}")
|
||||||
|
binding.contentView.context.showSessionDialog {
|
||||||
|
title("Delete Contact")
|
||||||
|
text("Are you sure you want to delete this contact?")
|
||||||
|
button(R.string.delete) {
|
||||||
|
val contacts = configFactory.contacts ?: return
|
||||||
|
contacts.upsertContact(contact.recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
cancelButton(::endActionMode)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() { binding.profilePictureView.recycle() }
|
||||||
binding.profilePictureView.recycle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HeaderViewHolder(
|
class HeaderViewHolder(
|
||||||
@ -52,15 +69,11 @@ class ContactListAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int { return items.size }
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
if (holder is ContactViewHolder) {
|
if (holder is ContactViewHolder) { holder.unbind() }
|
||||||
holder.unbind()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
@ -72,13 +85,9 @@ class ContactListAdapter(
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return if (viewType == ViewType.Contact) {
|
return if (viewType == ViewType.Contact) {
|
||||||
ContactViewHolder(
|
ContactViewHolder(ViewContactBinding.inflate(LayoutInflater.from(context), parent, false))
|
||||||
ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
HeaderViewHolder(
|
HeaderViewHolder(ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false))
|
||||||
ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
|
viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair())
|
||||||
}
|
}
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
private var unreadCount = 0
|
private var unreadCount = Int.MAX_VALUE
|
||||||
// Attachments
|
// Attachments
|
||||||
private val audioRecorder = AudioRecorder(this)
|
private val audioRecorder = AudioRecorder(this)
|
||||||
private val stopAudioHandler = Handler(Looper.getMainLooper())
|
private val stopAudioHandler = Handler(Looper.getMainLooper())
|
||||||
@ -575,7 +575,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
// The unreadCount check is to prevent us scrolling to the bottom when we first enter a conversation
|
||||||
|
if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE && unreadCount != Int.MAX_VALUE) {
|
||||||
scrollToMostRecentMessageIfWeShould()
|
scrollToMostRecentMessageIfWeShould()
|
||||||
}
|
}
|
||||||
handleRecyclerViewScrolled()
|
handleRecyclerViewScrolled()
|
||||||
@ -1251,6 +1252,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// `position` is the adapter position; not the visual position
|
// `position` is the adapter position; not the visual position
|
||||||
private fun handleSwipeToReply(message: MessageRecord) {
|
private fun handleSwipeToReply(message: MessageRecord) {
|
||||||
|
if (message.isOpenGroupInvitation) return
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
binding?.inputBar?.draftQuote(recipient, message, glide)
|
binding?.inputBar?.draftQuote(recipient, message, glide)
|
||||||
}
|
}
|
||||||
|
@ -532,7 +532,7 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
||||||
// Reply
|
// Reply
|
||||||
val canWrite = openGroup == null || openGroup.canWrite
|
val canWrite = openGroup == null || openGroup.canWrite
|
||||||
if (canWrite && !message.isPending && !message.isFailed) {
|
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
|
||||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
||||||
}
|
}
|
||||||
// Copy message text
|
// Copy message text
|
||||||
|
@ -123,7 +123,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
AppTheme {
|
AppTheme {
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
state = state,
|
state = state,
|
||||||
onReply = { setResultAndFinish(ON_REPLY) },
|
onReply = if (state.canReply) { { setResultAndFinish(ON_REPLY) } } else null,
|
||||||
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||||
onDelete = { setResultAndFinish(ON_DELETE) },
|
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||||
onClickImage = { viewModel.onClickImage(it) },
|
onClickImage = { viewModel.onClickImage(it) },
|
||||||
@ -145,7 +145,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MessageDetails(
|
fun MessageDetails(
|
||||||
state: MessageDetailsState,
|
state: MessageDetailsState,
|
||||||
onReply: () -> Unit = {},
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
onClickImage: (Int) -> Unit = {},
|
onClickImage: (Int) -> Unit = {},
|
||||||
@ -214,18 +214,20 @@ fun CellMetadata(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CellButtons(
|
fun CellButtons(
|
||||||
onReply: () -> Unit = {},
|
onReply: (() -> Unit)? = null,
|
||||||
onResend: (() -> Unit)? = null,
|
onResend: (() -> Unit)? = null,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Cell {
|
Cell {
|
||||||
Column {
|
Column {
|
||||||
ItemButton(
|
onReply?.let {
|
||||||
stringResource(R.string.reply),
|
ItemButton(
|
||||||
R.drawable.ic_message_details__reply,
|
stringResource(R.string.reply),
|
||||||
onClick = onReply
|
R.drawable.ic_message_details__reply,
|
||||||
)
|
onClick = it
|
||||||
Divider()
|
)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
onResend?.let {
|
onResend?.let {
|
||||||
ItemButton(
|
ItemButton(
|
||||||
stringResource(R.string.resend),
|
stringResource(R.string.resend),
|
||||||
|
@ -117,7 +117,7 @@ class MessageDetailsViewModel @Inject constructor(
|
|||||||
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
||||||
|
|
||||||
fun onClickImage(index: Int) {
|
fun onClickImage(index: Int) {
|
||||||
val state = state.value ?: return
|
val state = state.value
|
||||||
val mmsRecord = state.mmsRecord ?: return
|
val mmsRecord = state.mmsRecord ?: return
|
||||||
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
||||||
// only open to downloaded images
|
// only open to downloaded images
|
||||||
@ -158,6 +158,7 @@ data class MessageDetailsState(
|
|||||||
val thread: Recipient? = null,
|
val thread: Recipient? = null,
|
||||||
) {
|
) {
|
||||||
val fromTitle = GetString(R.string.message_details_header__from)
|
val fromTitle = GetString(R.string.message_details_header__from)
|
||||||
|
val canReply = record?.isOpenGroupInvitation != true
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
|
@ -140,6 +140,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
||||||
|
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
||||||
|
|
||||||
quote = message
|
quote = message
|
||||||
|
|
||||||
// If we already have a link preview View then clear the 'additional content' layout so that
|
// If we already have a link preview View then clear the 'additional content' layout so that
|
||||||
@ -178,7 +180,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
|
|||||||
// message we'll bail early if a link preview View already exists and just let
|
// message we'll bail early if a link preview View already exists and just let
|
||||||
// `updateLinkPreview` get called to update the existing View.
|
// `updateLinkPreview` get called to update the existing View.
|
||||||
if (linkPreview != null && linkPreviewDraftView != null) return
|
if (linkPreview != null && linkPreviewDraftView != null) return
|
||||||
|
linkPreviewDraftView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
||||||
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
|
linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this }
|
||||||
|
|
||||||
// Add the link preview View. Note: If there's already a quote View in the 'additional
|
// Add the link preview View. Note: If there's already a quote View in the 'additional
|
||||||
|
@ -77,7 +77,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
&& firstMessage.isMms && (firstMessage as MediaMmsMessageRecord).containsMediaSlide())
|
||||||
// Reply
|
// Reply
|
||||||
menu.findItem(R.id.menu_context_reply).isVisible =
|
menu.findItem(R.id.menu_context_reply).isVisible =
|
||||||
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed)
|
(selectedItems.size == 1 && !firstMessage.isPending && !firstMessage.isFailed && !firstMessage.isOpenGroupInvitation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
|
||||||
|
@ -37,6 +37,7 @@ import org.session.libsession.utilities.ViewUtil
|
|||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsession.utilities.modifyLayoutParams
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
@ -65,6 +66,7 @@ private const val TAG = "VisibleMessageView"
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class VisibleMessageView : LinearLayout {
|
class VisibleMessageView : LinearLayout {
|
||||||
|
private var replyDisabled: Boolean = false
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
|
||||||
@Inject lateinit var lokiApiDb: LokiAPIDatabase
|
@Inject lateinit var lokiApiDb: LokiAPIDatabase
|
||||||
@ -135,6 +137,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||||
lastSentMessageId: Long
|
lastSentMessageId: Long
|
||||||
) {
|
) {
|
||||||
|
replyDisabled = message.isOpenGroupInvitation
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||||
val isGroupThread = thread.isGroupRecipient
|
val isGroupThread = thread.isGroupRecipient
|
||||||
@ -206,7 +209,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||||
binding.dateBreakTextView.isVisible = showDateBreak
|
binding.dateBreakTextView.isVisible = showDateBreak
|
||||||
|
|
||||||
// Message status indicator
|
// Update message status indicator
|
||||||
showStatusMessage(message)
|
showStatusMessage(message)
|
||||||
|
|
||||||
// Emoji Reactions
|
// Emoji Reactions
|
||||||
@ -243,44 +246,101 @@ class VisibleMessageView : LinearLayout {
|
|||||||
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to display or hide the status of a message.
|
||||||
|
// Note: Although most commonly used to display the delivery status of a message, we also use the
|
||||||
|
// message status area to display the disappearing messages state - so in this latter case we'll
|
||||||
|
// be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the
|
||||||
|
// animated clock icon for incoming messages.
|
||||||
private fun showStatusMessage(message: MessageRecord) {
|
private fun showStatusMessage(message: MessageRecord) {
|
||||||
|
// We'll start by hiding everything and then only make visible what we need
|
||||||
|
binding.messageStatusTextView.isVisible = false
|
||||||
|
binding.messageStatusImageView.isVisible = false
|
||||||
|
binding.expirationTimerView.isVisible = false
|
||||||
|
|
||||||
val scheduledToDisappear = message.expiresIn > 0
|
// Get details regarding how we should display the message (it's delivery icon, icon tint colour, and
|
||||||
|
// the resource string for what text to display (R.string.delivery_status_sent etc.).
|
||||||
|
val (iconID, iconColor, textId) = getMessageStatusInfo(message)
|
||||||
|
|
||||||
|
// If we get any nulls then a message isn't one with a state that we care about (i.e., control messages
|
||||||
|
// etc.) - so bail. See: `DisplayRecord.is<WHATEVER>` for the full suite of message state methods.
|
||||||
|
// Also: We set all delivery status elements visibility to false just to make sure we don't display any
|
||||||
|
// stale data.
|
||||||
|
if (textId == null) return
|
||||||
|
|
||||||
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> {
|
binding.statusContainer.modifyLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
horizontalBias = if (message.isOutgoing) 1f else 0f
|
horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.expirationTimerView.isGone = true
|
// If the message is incoming AND it is not scheduled to disappear then don't show any status or timer details
|
||||||
|
val scheduledToDisappear = message.expiresIn > 0
|
||||||
|
if (message.isIncoming && !scheduledToDisappear) return
|
||||||
|
|
||||||
if (message.isOutgoing || scheduledToDisappear) {
|
// Set text & icons as appropriate for the message state. Note: Possible message states we care
|
||||||
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
// about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
|
||||||
textId?.let(binding.messageStatusTextView::setText)
|
textId.let(binding.messageStatusTextView::setText)
|
||||||
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
iconColor?.let(binding.messageStatusTextView::setTextColor)
|
||||||
iconID?.let { ContextCompat.getDrawable(context, it) }
|
iconID?.let { ContextCompat.getDrawable(context, it) }
|
||||||
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
|
||||||
?.let(binding.messageStatusImageView::setImageDrawable)
|
?.let(binding.messageStatusImageView::setImageDrawable)
|
||||||
|
|
||||||
// Always show the delivery status of the last sent message
|
// Potential options at this point are that the message is:
|
||||||
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
// i.) incoming AND scheduled to disappear.
|
||||||
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
|
// ii.) outgoing but NOT scheduled to disappear, or
|
||||||
val isLastSentMessage = lastSentMessageId == message.id
|
// iii.) outgoing AND scheduled to disappear.
|
||||||
|
|
||||||
binding.messageStatusTextView.isVisible = textId != null && (isLastSentMessage || scheduledToDisappear)
|
// ----- Case i..) Message is incoming and scheduled to disappear -----
|
||||||
val showTimer = scheduledToDisappear && !message.isPending
|
if (message.isIncoming && scheduledToDisappear) {
|
||||||
binding.messageStatusImageView.isVisible = iconID != null && !showTimer && (!message.isSent || isLastSentMessage)
|
// Display the status ('Read') and the show the timer only (no delivery icon)
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
binding.messageStatusImageView.bringToFront()
|
binding.expirationTimerView.isVisible = true
|
||||||
binding.expirationTimerView.bringToFront()
|
binding.expirationTimerView.bringToFront()
|
||||||
binding.expirationTimerView.isVisible = showTimer
|
updateExpirationTimer(message)
|
||||||
if (showTimer) updateExpirationTimer(message)
|
return
|
||||||
} else {
|
}
|
||||||
binding.messageStatusTextView.isVisible = false
|
|
||||||
binding.messageStatusImageView.isVisible = false
|
// --- If we got here then we know the message is outgoing ---
|
||||||
|
|
||||||
|
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
|
||||||
|
val isLastSentMessage = lastSentMessageId == message.id
|
||||||
|
|
||||||
|
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
|
||||||
|
if (!scheduledToDisappear) {
|
||||||
|
// If this isn't a disappearing message then we never show the timer
|
||||||
|
|
||||||
|
// If the message has NOT been successfully sent then always show the delivery status text and icon..
|
||||||
|
val neitherSentNorRead = !(message.isSent || message.isRead)
|
||||||
|
if (neitherSentNorRead) {
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
|
binding.messageStatusImageView.isVisible = true
|
||||||
|
} else {
|
||||||
|
// ..but if the message HAS been successfully sent or read then only display the delivery status
|
||||||
|
// text and image if this is the last sent message.
|
||||||
|
binding.messageStatusTextView.isVisible = isLastSentMessage
|
||||||
|
binding.messageStatusImageView.isVisible = isLastSentMessage
|
||||||
|
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
|
||||||
|
{
|
||||||
|
// Always display the delivery status text on all outgoing disappearing messages
|
||||||
|
binding.messageStatusTextView.isVisible = true
|
||||||
|
|
||||||
|
// If the message is sent or has been read..
|
||||||
|
val sentOrRead = message.isSent || message.isRead
|
||||||
|
if (sentOrRead) {
|
||||||
|
// ..then display the timer icon for this disappearing message (but keep the message status icon hidden)
|
||||||
|
binding.expirationTimerView.isVisible = true
|
||||||
|
binding.expirationTimerView.bringToFront()
|
||||||
|
updateExpirationTimer(message)
|
||||||
|
} else {
|
||||||
|
// If the message has NOT been sent or read (or it has failed) then show the delivery status icon rather than the timer icon
|
||||||
|
binding.messageStatusImageView.isVisible = true
|
||||||
|
binding.messageStatusImageView.bringToFront()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,10 +362,9 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@ColorInt val iconTint: Int?,
|
@ColorInt val iconTint: Int?,
|
||||||
@StringRes val messageText: Int?)
|
@StringRes val messageText: Int?)
|
||||||
|
|
||||||
private fun getMessageStatusImage(message: MessageRecord): MessageStatusInfo = when {
|
private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when {
|
||||||
message.isFailed ->
|
message.isFailed ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
|
||||||
R.drawable.ic_delivery_status_failed,
|
|
||||||
resources.getColor(R.color.destructive, context.theme),
|
resources.getColor(R.color.destructive, context.theme),
|
||||||
R.string.delivery_status_failed
|
R.string.delivery_status_failed
|
||||||
)
|
)
|
||||||
@ -318,24 +377,32 @@ class VisibleMessageView : LinearLayout {
|
|||||||
message.isPending ->
|
message.isPending ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_sending
|
||||||
)
|
)
|
||||||
message.isResyncing ->
|
message.isSyncing || message.isResyncing ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColor(R.color.accent_orange), R.string.delivery_status_syncing
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending"
|
||||||
)
|
)
|
||||||
message.isRead || !message.isOutgoing ->
|
message.isRead || message.isIncoming ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_read,
|
R.drawable.ic_delivery_status_read,
|
||||||
context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.delivery_status_read
|
||||||
)
|
)
|
||||||
else ->
|
message.isSent ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sent,
|
R.drawable.ic_delivery_status_sent,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.delivery_status_sent
|
R.string.delivery_status_sent
|
||||||
)
|
)
|
||||||
|
else -> {
|
||||||
|
// The message isn't one we care about for message statuses we display to the user (i.e.,
|
||||||
|
// control messages etc. - see the `DisplayRecord.is<WHATEVER>` suite of methods for options).
|
||||||
|
MessageStatusInfo(null, null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
@ -409,6 +476,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
}
|
}
|
||||||
|
if (replyDisabled) return
|
||||||
if (translationX > 0) { return } // Only allow swipes to the left
|
if (translationX > 0) { return } // Only allow swipes to the left
|
||||||
// The idea here is to asymptotically approach a maximum drag distance
|
// The idea here is to asymptotically approach a maximum drag distance
|
||||||
val damping = 50.0f
|
val damping = 50.0f
|
||||||
|
@ -241,7 +241,21 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void selectDocument(Activity activity, int requestCode) {
|
public static void selectDocument(Activity activity, int requestCode) {
|
||||||
selectMediaType(activity, "*/*", null, requestCode);
|
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||||
|
|
||||||
|
// The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on
|
||||||
|
// Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||||
|
.request(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
|
.request(Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
} else {
|
||||||
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
|
}
|
||||||
|
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||||
|
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
||||||
|
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.util.Range
|
import android.util.Range
|
||||||
|
import androidx.appcompat.widget.ThemeUtils
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.combine.Tuple2
|
import nl.komponents.kovenant.combine.Tuple2
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
|
import org.thoughtcrime.securesms.util.getColorResourceIdFromAttr
|
||||||
|
import org.thoughtcrime.securesms.util.getMessageTextColourAttr
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
object MentionUtilities {
|
object MentionUtilities {
|
||||||
@ -58,15 +65,37 @@ object MentionUtilities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = SpannableString(text)
|
val result = SpannableString(text)
|
||||||
val isLightMode = UiModeUtilities.isDayUiMode(context)
|
|
||||||
val color = if (isOutgoingMessage) {
|
var mentionTextColour: Int? = null
|
||||||
ResourcesCompat.getColor(context.resources, if (isLightMode) R.color.white else R.color.black, context.theme)
|
// In dark themes..
|
||||||
} else {
|
if (ThemeUtil.isDarkTheme(context)) {
|
||||||
context.getAccentColor()
|
// ..we use the standard outgoing message colour for outgoing messages..
|
||||||
|
if (isOutgoingMessage) {
|
||||||
|
val mentionTextColourAttributeId = getMessageTextColourAttr(true)
|
||||||
|
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||||
|
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||||
|
}
|
||||||
|
else // ..but we use the accent colour for incoming messages (i.e., someone mentioning us)..
|
||||||
|
{
|
||||||
|
mentionTextColour = context.getAccentColor()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else // ..while in light themes we always just use the incoming or outgoing message text colour for mentions.
|
||||||
|
{
|
||||||
|
val mentionTextColourAttributeId = getMessageTextColourAttr(isOutgoingMessage)
|
||||||
|
val mentionTextColourResourceId = getColorResourceIdFromAttr(context, mentionTextColourAttributeId)
|
||||||
|
mentionTextColour = ResourcesCompat.getColor(context.resources, mentionTextColourResourceId, context.theme)
|
||||||
|
}
|
||||||
|
|
||||||
for (mention in mentions) {
|
for (mention in mentions) {
|
||||||
result.setSpan(ForegroundColorSpan(color), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
result.setSpan(ForegroundColorSpan(mentionTextColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
|
||||||
|
// If we're using a light theme then we change the background colour of the mention to be the accent colour
|
||||||
|
if (ThemeUtil.isLightTheme(context)) {
|
||||||
|
val backgroundColour = context.getAccentColor();
|
||||||
|
result.setSpan(BackgroundColorSpan(backgroundColour), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -320,6 +320,19 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLastMessageTimestamp(long threadId) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
|
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getUnread() {
|
public Cursor getUnread() {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " ASC";
|
||||||
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0";
|
||||||
|
@ -22,15 +22,11 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.session.libsession.messaging.calls.CallMessageType;
|
import org.session.libsession.messaging.calls.CallMessageType;
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||||
@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -634,7 +629,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
long threadId = getThreadIdForMessage(messageId);
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
|
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, false);
|
||||||
return threadDeleted;
|
return threadDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -701,12 +696,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package */void deleteThread(long threadId) {
|
void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
|
||||||
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
|
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
|
||||||
|
|
||||||
@ -719,7 +709,12 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ void deleteThreads(Set<Long> threadIds) {
|
void deleteThread(long threadId) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteThreads(Set<Long> threadIds) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = "";
|
String where = "";
|
||||||
|
|
||||||
@ -727,23 +722,23 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
where += THREAD_ID + " = '" + threadId + "' OR ";
|
where += THREAD_ID + " = '" + threadId + "' OR ";
|
||||||
}
|
}
|
||||||
|
|
||||||
where = where.substring(0, where.length() - 4);
|
where = where.substring(0, where.length() - 4); // Remove the final: "' OR "
|
||||||
|
|
||||||
db.delete(TABLE_NAME, where, null);
|
db.delete(TABLE_NAME, where, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package */ void deleteAllThreads() {
|
void deleteAllThreads() {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(TABLE_NAME, null, null);
|
db.delete(TABLE_NAME, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ SQLiteDatabase beginTransaction() {
|
SQLiteDatabase beginTransaction() {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
database.beginTransaction();
|
database.beginTransaction();
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ void endTransaction(SQLiteDatabase database) {
|
void endTransaction(SQLiteDatabase database) {
|
||||||
database.setTransactionSuccessful();
|
database.setTransactionSuccessful();
|
||||||
database.endTransaction();
|
database.endTransaction();
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ private const val TAG = "Storage"
|
|||||||
open class Storage(
|
open class Storage(
|
||||||
context: Context,
|
context: Context,
|
||||||
helper: SQLCipherOpenHelper,
|
helper: SQLCipherOpenHelper,
|
||||||
private val configFactory: ConfigFactory
|
val configFactory: ConfigFactory
|
||||||
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
|
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
|
||||||
|
|
||||||
override fun threadCreated(address: Address, threadId: Long) {
|
override fun threadCreated(address: Address, threadId: Long) {
|
||||||
@ -1371,29 +1371,29 @@ open class Storage(
|
|||||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||||
val groupDB = DatabaseComponent.get(context).groupDatabase()
|
val groupDB = DatabaseComponent.get(context).groupDatabase()
|
||||||
threadDB.deleteConversation(threadID)
|
threadDB.deleteConversation(threadID)
|
||||||
val recipient = getRecipientForThread(threadID) ?: return
|
|
||||||
when {
|
val recipient = getRecipientForThread(threadID)
|
||||||
recipient.isContactRecipient -> {
|
if (recipient == null) {
|
||||||
if (recipient.isLocalNumber) return
|
Log.w(TAG, "Got null recipient when deleting conversation - aborting.");
|
||||||
val contacts = configFactory.contacts ?: return
|
return
|
||||||
contacts.upsertContact(recipient.address.serialize()) { priority = PRIORITY_HIDDEN }
|
}
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
|
||||||
}
|
// There is nothing further we need to do if this is a 1-on-1 conversation, and it's not
|
||||||
recipient.isClosedGroupRecipient -> {
|
// possible to delete communities in this manner so bail.
|
||||||
// TODO: handle closed group
|
if (recipient.isContactRecipient || recipient.isCommunityRecipient) return
|
||||||
val volatile = configFactory.convoVolatile ?: return
|
|
||||||
val groups = configFactory.userGroups ?: return
|
// If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient)
|
||||||
val groupID = recipient.address.toGroupString()
|
val volatile = configFactory.convoVolatile ?: return
|
||||||
val closedGroup = getGroup(groupID)
|
val groups = configFactory.userGroups ?: return
|
||||||
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
|
val groupID = recipient.address.toGroupString()
|
||||||
if (closedGroup != null) {
|
val closedGroup = getGroup(groupID)
|
||||||
groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
|
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
|
||||||
volatile.eraseLegacyClosedGroup(groupPublicKey)
|
if (closedGroup != null) {
|
||||||
groups.eraseLegacyGroup(groupPublicKey)
|
groupDB.delete(groupID)
|
||||||
} else {
|
volatile.eraseLegacyClosedGroup(groupPublicKey)
|
||||||
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
|
groups.eraseLegacyGroup(groupPublicKey)
|
||||||
}
|
} else {
|
||||||
}
|
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,14 +26,10 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.MergeCursor;
|
import android.database.MergeCursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.snode.SnodeAPI;
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@ -61,7 +57,6 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -83,7 +78,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public static final String TABLE_NAME = "thread";
|
public static final String TABLE_NAME = "thread";
|
||||||
public static final String ID = "_id";
|
public static final String ID = "_id";
|
||||||
public static final String DATE = "date";
|
public static final String THREAD_CREATION_DATE = "date";
|
||||||
public static final String MESSAGE_COUNT = "message_count";
|
public static final String MESSAGE_COUNT = "message_count";
|
||||||
public static final String ADDRESS = "recipient_ids";
|
public static final String ADDRESS = "recipient_ids";
|
||||||
public static final String SNIPPET = "snippet";
|
public static final String SNIPPET = "snippet";
|
||||||
@ -91,7 +86,7 @@ public class ThreadDatabase extends Database {
|
|||||||
public static final String READ = "read";
|
public static final String READ = "read";
|
||||||
public static final String UNREAD_COUNT = "unread_count";
|
public static final String UNREAD_COUNT = "unread_count";
|
||||||
public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
|
public static final String UNREAD_MENTION_COUNT = "unread_mention_count";
|
||||||
public static final String TYPE = "type";
|
public static final String DISTRIBUTION_TYPE = "type"; // See: DistributionTypes.kt
|
||||||
private static final String ERROR = "error";
|
private static final String ERROR = "error";
|
||||||
public static final String SNIPPET_TYPE = "snippet_type";
|
public static final String SNIPPET_TYPE = "snippet_type";
|
||||||
public static final String SNIPPET_URI = "snippet_uri";
|
public static final String SNIPPET_URI = "snippet_uri";
|
||||||
@ -101,27 +96,27 @@ public class ThreadDatabase extends Database {
|
|||||||
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
||||||
public static final String EXPIRES_IN = "expires_in";
|
public static final String EXPIRES_IN = "expires_in";
|
||||||
public static final String LAST_SEEN = "last_seen";
|
public static final String LAST_SEEN = "last_seen";
|
||||||
public static final String HAS_SENT = "has_sent";
|
public static final String HAS_SENT = "has_sent";
|
||||||
public static final String IS_PINNED = "is_pinned";
|
public static final String IS_PINNED = "is_pinned";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
||||||
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
|
ID + " INTEGER PRIMARY KEY, " + THREAD_CREATION_DATE + " INTEGER DEFAULT 0, " +
|
||||||
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " +
|
MESSAGE_COUNT + " INTEGER DEFAULT 0, " + ADDRESS + " TEXT, " + SNIPPET + " TEXT, " +
|
||||||
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
|
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
|
||||||
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
|
DISTRIBUTION_TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
|
||||||
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
|
||||||
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
|
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
|
||||||
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||||
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||||
|
|
||||||
public static final String[] CREATE_INDEXS = {
|
public static final String[] CREATE_INDEXES = {
|
||||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
|
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
|
||||||
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String[] THREAD_PROJECTION = {
|
private static final String[] THREAD_PROJECTION = {
|
||||||
ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, TYPE, ERROR, SNIPPET_TYPE,
|
ID, THREAD_CREATION_DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, UNREAD_MENTION_COUNT, DISTRIBUTION_TYPE, ERROR, SNIPPET_TYPE,
|
||||||
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
|
SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, IS_PINNED
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,8 +126,8 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
private static final List<String> COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
|
private static final List<String> COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
|
||||||
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
|
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
|
||||||
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
public static String getCreatePinnedCommand() {
|
public static String getCreatePinnedCommand() {
|
||||||
return "ALTER TABLE "+ TABLE_NAME + " " +
|
return "ALTER TABLE "+ TABLE_NAME + " " +
|
||||||
@ -158,11 +153,10 @@ public class ThreadDatabase extends Database {
|
|||||||
ContentValues contentValues = new ContentValues(4);
|
ContentValues contentValues = new ContentValues(4);
|
||||||
long date = SnodeAPI.getNowWithOffset();
|
long date = SnodeAPI.getNowWithOffset();
|
||||||
|
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
contentValues.put(ADDRESS, address.serialize());
|
contentValues.put(ADDRESS, address.serialize());
|
||||||
|
|
||||||
if (group)
|
if (group) contentValues.put(DISTRIBUTION_TYPE, distributionType);
|
||||||
contentValues.put(TYPE, distributionType);
|
|
||||||
|
|
||||||
contentValues.put(MESSAGE_COUNT, 0);
|
contentValues.put(MESSAGE_COUNT, 0);
|
||||||
|
|
||||||
@ -175,7 +169,7 @@ public class ThreadDatabase extends Database {
|
|||||||
long expiresIn, int readReceiptCount)
|
long expiresIn, int readReceiptCount)
|
||||||
{
|
{
|
||||||
ContentValues contentValues = new ContentValues(7);
|
ContentValues contentValues = new ContentValues(7);
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
contentValues.put(MESSAGE_COUNT, count);
|
contentValues.put(MESSAGE_COUNT, count);
|
||||||
if (!body.isEmpty()) {
|
if (!body.isEmpty()) {
|
||||||
contentValues.put(SNIPPET, body);
|
contentValues.put(SNIPPET, body);
|
||||||
@ -187,9 +181,7 @@ public class ThreadDatabase extends Database {
|
|||||||
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
|
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
|
||||||
contentValues.put(EXPIRES_IN, expiresIn);
|
contentValues.put(EXPIRES_IN, expiresIn);
|
||||||
|
|
||||||
if (unarchive) {
|
if (unarchive) { contentValues.put(ARCHIVED, 0); }
|
||||||
contentValues.put(ARCHIVED, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
|
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
|
||||||
@ -199,7 +191,7 @@ public class ThreadDatabase extends Database {
|
|||||||
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
|
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
|
||||||
ContentValues contentValues = new ContentValues(4);
|
ContentValues contentValues = new ContentValues(4);
|
||||||
|
|
||||||
contentValues.put(DATE, date - date % 1000);
|
contentValues.put(THREAD_CREATION_DATE, date - date % 1000);
|
||||||
if (!snippet.isEmpty()) {
|
if (!snippet.isEmpty()) {
|
||||||
contentValues.put(SNIPPET, snippet);
|
contentValues.put(SNIPPET, snippet);
|
||||||
}
|
}
|
||||||
@ -230,9 +222,7 @@ public class ThreadDatabase extends Database {
|
|||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
String where = "";
|
String where = "";
|
||||||
|
|
||||||
for (long threadId : threadIds) {
|
for (long threadId : threadIds) { where += ID + " = '" + threadId + "' OR "; }
|
||||||
where += ID + " = '" + threadId + "' OR ";
|
|
||||||
}
|
|
||||||
|
|
||||||
where = where.substring(0, where.length() - 4);
|
where = where.substring(0, where.length() - 4);
|
||||||
|
|
||||||
@ -358,7 +348,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public void setDistributionType(long threadId, int distributionType) {
|
public void setDistributionType(long threadId, int distributionType) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(TYPE, distributionType);
|
contentValues.put(DISTRIBUTION_TYPE, distributionType);
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||||
@ -367,7 +357,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public void setDate(long threadId, long date) {
|
public void setDate(long threadId, long date) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(DATE, date);
|
contentValues.put(THREAD_CREATION_DATE, date);
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
||||||
if (updated > 0) notifyConversationListListeners();
|
if (updated > 0) notifyConversationListListeners();
|
||||||
@ -375,11 +365,11 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public int getDistributionType(long threadId) {
|
public int getDistributionType(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, new String[]{DISTRIBUTION_TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
return cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
return cursor.getInt(cursor.getColumnIndexOrThrow(DISTRIBUTION_TYPE));
|
||||||
}
|
}
|
||||||
|
|
||||||
return DistributionTypes.DEFAULT;
|
return DistributionTypes.DEFAULT;
|
||||||
@ -469,7 +459,7 @@ public class ThreadDatabase extends Database {
|
|||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String where = "SELECT " + DATE + " FROM " + TABLE_NAME +
|
String where = "SELECT " + THREAD_CREATION_DATE + " FROM " + TABLE_NAME +
|
||||||
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME +
|
||||||
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
|
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
|
||||||
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
||||||
@ -477,7 +467,7 @@ public class ThreadDatabase extends Database {
|
|||||||
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
|
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
|
||||||
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
|
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
|
||||||
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
|
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
|
||||||
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + DATE + " DESC LIMIT 1";
|
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL ORDER BY " + THREAD_CREATION_DATE + " DESC LIMIT 1";
|
||||||
cursor = db.rawQuery(where, null);
|
cursor = db.rawQuery(where, null);
|
||||||
|
|
||||||
if (cursor != null && cursor.moveToFirst())
|
if (cursor != null && cursor.moveToFirst())
|
||||||
@ -595,7 +585,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public Long getLastUpdated(long threadId) {
|
public Long getLastUpdated(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, new String[]{DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, new String[]{THREAD_CREATION_DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
@ -736,7 +726,7 @@ public class ThreadDatabase extends Database {
|
|||||||
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
long count = mmsSmsDatabase.getConversationCount(threadId);
|
long count = mmsSmsDatabase.getConversationCount(threadId);
|
||||||
|
|
||||||
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId);
|
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && possibleToDeleteThreadOnEmpty(threadId);
|
||||||
|
|
||||||
if (count == 0 && shouldDeleteEmptyThread) {
|
if (count == 0 && shouldDeleteEmptyThread) {
|
||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
@ -810,7 +800,7 @@ public class ThreadDatabase extends Database {
|
|||||||
return setLastSeen(threadId, lastSeenTime);
|
return setLastSeen(threadId, lastSeenTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteThreadOnEmpty(long threadId) {
|
private boolean possibleToDeleteThreadOnEmpty(long threadId) {
|
||||||
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||||
return threadRecipient != null && !threadRecipient.isCommunityRecipient();
|
return threadRecipient != null && !threadRecipient.isCommunityRecipient();
|
||||||
}
|
}
|
||||||
@ -855,7 +845,7 @@ public class ThreadDatabase extends Database {
|
|||||||
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
|
||||||
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
|
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
|
||||||
" WHERE " + where +
|
" WHERE " + where +
|
||||||
" ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + DATE + " DESC";
|
" ORDER BY " + TABLE_NAME + "." + IS_PINNED + " DESC, " + TABLE_NAME + "." + THREAD_CREATION_DATE + " DESC";
|
||||||
|
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
query += " LIMIT " + limit;
|
query += " LIMIT " + limit;
|
||||||
@ -900,7 +890,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
public ThreadRecord getCurrent() {
|
public ThreadRecord getCurrent() {
|
||||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
|
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
|
||||||
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE));
|
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DISTRIBUTION_TYPE));
|
||||||
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS)));
|
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS)));
|
||||||
|
|
||||||
Optional<RecipientSettings> settings;
|
Optional<RecipientSettings> settings;
|
||||||
@ -916,7 +906,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
|
Recipient recipient = Recipient.from(context, address, settings, groupRecord, true);
|
||||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
|
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
|
||||||
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
|
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.THREAD_CREATION_DATE));
|
||||||
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
|
||||||
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT));
|
||||||
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
|
int unreadMentionCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_MENTION_COUNT));
|
||||||
@ -934,7 +924,17 @@ public class ThreadDatabase extends Database {
|
|||||||
readReceiptCount = 0;
|
readReceiptCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ThreadRecord(body, snippetUri, recipient, date, count,
|
MessageRecord lastMessage = null;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||||
|
long messageTimestamp = mmsSmsDatabase.getLastMessageTimestamp(threadId);
|
||||||
|
if (messageTimestamp > 0) {
|
||||||
|
lastMessage = mmsSmsDatabase.getMessageForTimestamp(messageTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThreadRecord(body, snippetUri, lastMessage, recipient, date, count,
|
||||||
unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type,
|
unreadCount, unreadMentionCount, threadId, deliveryReceiptCount, status, type,
|
||||||
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
|
distributionType, archived, expiresIn, lastSeen, readReceiptCount, pinned);
|
||||||
}
|
}
|
||||||
|
@ -357,7 +357,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
|
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
|
executeStatements(db, ThreadDatabase.CREATE_INDEXES);
|
||||||
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||||
|
@ -78,8 +78,8 @@ public abstract class DisplayRecord {
|
|||||||
public int getReadReceiptCount() { return readReceiptCount; }
|
public int getReadReceiptCount() { return readReceiptCount; }
|
||||||
|
|
||||||
public boolean isDelivered() {
|
public boolean isDelivered() {
|
||||||
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE
|
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
|
||||||
&& deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
|
public boolean isSent() { return MmsSmsColumns.Types.isSentType(type); }
|
||||||
@ -114,6 +114,11 @@ public abstract class DisplayRecord {
|
|||||||
public boolean isOutgoing() {
|
public boolean isOutgoing() {
|
||||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIncoming() {
|
||||||
|
return !MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isGroupUpdateMessage() {
|
public boolean isGroupUpdateMessage() {
|
||||||
return SmsDatabase.Types.isGroupUpdateMessage(type);
|
return SmsDatabase.Types.isGroupUpdateMessage(type);
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData;
|
|||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.NetworkFailure;
|
import org.session.libsession.utilities.NetworkFailure;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -120,7 +121,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
|
||||||
} else if (isExpirationTimerUpdate()) {
|
} else if (isExpirationTimerUpdate()) {
|
||||||
int seconds = (int) (getExpiresIn() / 1000);
|
int seconds = (int) (getExpiresIn() / 1000);
|
||||||
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getRecipient(), getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted));
|
boolean isGroup = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(getThreadId()).isGroupRecipient();
|
||||||
|
return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, isGroup, getIndividualRecipient().getAddress().serialize(), isOutgoing(), getTimestamp(), expireStarted));
|
||||||
} else if (isDataExtractionNotification()) {
|
} else if (isDataExtractionNotification()) {
|
||||||
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
if (isScreenshotNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
|
||||||
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
else if (isMediaSavedNotification()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
|
||||||
|
@ -43,6 +43,7 @@ import network.loki.messenger.R;
|
|||||||
public class ThreadRecord extends DisplayRecord {
|
public class ThreadRecord extends DisplayRecord {
|
||||||
|
|
||||||
private @Nullable final Uri snippetUri;
|
private @Nullable final Uri snippetUri;
|
||||||
|
public @Nullable final MessageRecord lastMessage;
|
||||||
private final long count;
|
private final long count;
|
||||||
private final int unreadCount;
|
private final int unreadCount;
|
||||||
private final int unreadMentionCount;
|
private final int unreadMentionCount;
|
||||||
@ -54,13 +55,14 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
private final int initialRecipientHash;
|
private final int initialRecipientHash;
|
||||||
|
|
||||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||||
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
||||||
long snippetType, int distributionType, boolean archived, long expiresIn,
|
long snippetType, int distributionType, boolean archived, long expiresIn,
|
||||||
long lastSeen, int readReceiptCount, boolean pinned)
|
long lastSeen, int readReceiptCount, boolean pinned)
|
||||||
{
|
{
|
||||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||||
this.snippetUri = snippetUri;
|
this.snippetUri = snippetUri;
|
||||||
|
this.lastMessage = lastMessage;
|
||||||
this.count = count;
|
this.count = count;
|
||||||
this.unreadCount = unreadCount;
|
this.unreadCount = unreadCount;
|
||||||
this.unreadMentionCount = unreadMentionCount;
|
this.unreadMentionCount = unreadMentionCount;
|
||||||
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.TextUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -89,7 +91,7 @@ class ConversationView : LinearLayout {
|
|||||||
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
|
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
|
||||||
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
|
||||||
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
|
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
|
||||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
val senderDisplayName = getTitle(thread.recipient)
|
||||||
?: thread.recipient.address.toString()
|
?: thread.recipient.address.toString()
|
||||||
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
binding.conversationViewDisplayNameTextView.text = senderDisplayName
|
||||||
binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) }
|
binding.timestampTextView.text = thread.date.takeIf { it != 0L }?.let { DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), it) }
|
||||||
@ -101,9 +103,7 @@ class ConversationView : LinearLayout {
|
|||||||
R.drawable.ic_notifications_mentions
|
R.drawable.ic_notifications_mentions
|
||||||
}
|
}
|
||||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
val rawSnippet = thread.getDisplayBody(context)
|
binding.snippetTextView.text = highlightMentions(thread.getSnippet(), thread.threadId, context)
|
||||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
|
||||||
binding.snippetTextView.text = snippet
|
|
||||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
@ -131,12 +131,21 @@ class ConversationView : LinearLayout {
|
|||||||
binding.profilePictureView.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getTitle(recipient: Recipient): String? = when {
|
||||||
return if (recipient.isLocalNumber) {
|
recipient.isLocalNumber -> context.getString(R.string.note_to_self)
|
||||||
context.getString(R.string.note_to_self)
|
else -> recipient.toShortString() // Internally uses the Contact API
|
||||||
} else {
|
}
|
||||||
recipient.toShortString() // Internally uses the Contact API
|
|
||||||
}
|
private fun ThreadRecord.getSnippet(): CharSequence =
|
||||||
|
concatSnippet(getSnippetPrefix(), getDisplayBody(context))
|
||||||
|
|
||||||
|
private fun concatSnippet(prefix: CharSequence?, body: CharSequence): CharSequence =
|
||||||
|
prefix?.let { TextUtils.concat(it, ": ", body) } ?: body
|
||||||
|
|
||||||
|
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||||
|
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
||||||
|
lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you)
|
||||||
|
else -> lastMessage?.individualRecipient?.toShortString()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.home.search
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.InputFilter.LengthFilter
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@ -34,6 +36,7 @@ class GlobalSearchInputLayout @JvmOverloads constructor(
|
|||||||
binding.searchInput.onFocusChangeListener = this
|
binding.searchInput.onFocusChangeListener = this
|
||||||
binding.searchInput.addTextChangedListener(this)
|
binding.searchInput.addTextChangedListener(this)
|
||||||
binding.searchInput.setOnEditorActionListener(this)
|
binding.searchInput.setOnEditorActionListener(this)
|
||||||
|
binding.searchInput.setFilters( arrayOf<InputFilter>(LengthFilter(100)) ) // 100 char search limit
|
||||||
binding.searchCancel.setOnClickListener(this)
|
binding.searchCancel.setOnClickListener(this)
|
||||||
binding.searchClear.setOnClickListener(this)
|
binding.searchClear.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,7 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
|
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
private val executor = viewModelScope + SupervisorJob()
|
||||||
|
|
||||||
private val _result: MutableStateFlow<GlobalSearchResult> =
|
private val _result: MutableStateFlow<GlobalSearchResult> = MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||||
MutableStateFlow(GlobalSearchResult.EMPTY)
|
|
||||||
|
|
||||||
val result: StateFlow<GlobalSearchResult> = _result
|
val result: StateFlow<GlobalSearchResult> = _result
|
||||||
|
|
||||||
@ -41,13 +40,14 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
_queryText
|
_queryText
|
||||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
.mapLatest { query ->
|
.mapLatest { query ->
|
||||||
if (query.trim().length < 2) {
|
// Early exit on empty search query
|
||||||
|
if (query.trim().isEmpty()) {
|
||||||
SearchResult.EMPTY
|
SearchResult.EMPTY
|
||||||
} else {
|
} else {
|
||||||
// user input delay here in case we get a new query within a few hundred ms
|
// User input delay in case we get a new query within a few hundred ms this
|
||||||
// this coroutine will be cancelled and expensive query will not be run if typing quickly
|
// coroutine will be cancelled and the expensive query will not be run.
|
||||||
// first query of 2 characters will be instant however
|
|
||||||
delay(300)
|
delay(300)
|
||||||
|
|
||||||
val settableFuture = SettableFuture<SearchResult>()
|
val settableFuture = SettableFuture<SearchResult>()
|
||||||
searchRepository.query(query.toString(), settableFuture::set)
|
searchRepository.query(query.toString(), settableFuture::set)
|
||||||
try {
|
try {
|
||||||
@ -64,6 +64,4 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
}
|
}
|
||||||
.launchIn(executor)
|
.launchIn(executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -349,11 +349,17 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
builder.setThread(notifications.get(0).getRecipient());
|
builder.setThread(notifications.get(0).getRecipient());
|
||||||
builder.setMessageCount(notificationState.getMessageCount());
|
builder.setMessageCount(notificationState.getMessageCount());
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(notifications.get(0).getThreadId(),context);
|
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(notifications.get(0).getThreadId(),context);
|
||||||
|
|
||||||
|
// TODO: Removing highlighting mentions in the notification because this context is the libsession one which
|
||||||
|
// TODO: doesn't have access to the `R.attr.message_sent_text_color` and `R.attr.message_received_text_color`
|
||||||
|
// TODO: attributes to perform the colour lookup. Also, it makes little sense to highlight the mentions using
|
||||||
|
// TODO: the app theme as it may result in insufficient contrast with the notification background which will
|
||||||
|
// TODO: be using the SYSTEM theme.
|
||||||
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
|
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
|
||||||
MentionUtilities.highlightMentions(text == null ? "" : text,
|
//MentionUtilities.highlightMentions(text == null ? "" : text, notifications.get(0).getThreadId(), context), // Removing hightlighting mentions -ACL
|
||||||
notifications.get(0).getThreadId(),
|
text == null ? "" : text,
|
||||||
context),
|
|
||||||
notifications.get(0).getSlideDeck());
|
notifications.get(0).getSlideDeck());
|
||||||
|
|
||||||
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
||||||
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
|
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
|
||||||
builder.setOnlyAlertOnce(!signal);
|
builder.setOnlyAlertOnce(!signal);
|
||||||
|
@ -61,11 +61,15 @@ class MarkReadReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
|
|
||||||
|
val threadDb = DatabaseComponent.get(context).threadDatabase()
|
||||||
|
|
||||||
// start disappear after read messages except TimerUpdates in groups.
|
// start disappear after read messages except TimerUpdates in groups.
|
||||||
markedReadMessages
|
markedReadMessages
|
||||||
.filter { it.expiryType == ExpiryType.AFTER_READ }
|
.filter { it.expiryType == ExpiryType.AFTER_READ }
|
||||||
.map { it.syncMessageId }
|
.map { it.syncMessageId }
|
||||||
.filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run { isExpirationTimerUpdate && recipient.isClosedGroupRecipient } == false }
|
.filter { mmsSmsDatabase.getMessageForTimestamp(it.timetamp)?.run {
|
||||||
|
isExpirationTimerUpdate && threadDb.getRecipientForThreadId(threadId)?.isGroupRecipient == true } == false
|
||||||
|
}
|
||||||
.forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) }
|
.forEach { messageExpirationManager.startDisappearAfterRead(it.timetamp, it.address.serialize()) }
|
||||||
|
|
||||||
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
|
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
|
||||||
|
@ -38,6 +38,7 @@ import org.session.libsession.snode.SnodeAPI
|
|||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.getProperty
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
@ -119,7 +120,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
passwordButton.isGone = prefs.getHidePassword()
|
passwordButton.isGone = prefs.getHidePassword()
|
||||||
passwordButton.setOnClickListener { showPassword() }
|
passwordButton.setOnClickListener { showPassword() }
|
||||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||||
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
|
||||||
|
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
|
||||||
|
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,19 +4,14 @@ import network.loki.messenger.libsession_util.util.ExpiryMode
|
|||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import app.cash.copper.Query
|
import app.cash.copper.Query
|
||||||
import app.cash.copper.flow.observeQuery
|
import app.cash.copper.flow.observeQuery
|
||||||
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||||
@ -32,9 +27,7 @@ import org.session.libsession.utilities.GroupUtil
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase
|
import org.thoughtcrime.securesms.database.DraftDatabase
|
||||||
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
|
||||||
@ -51,7 +44,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface ConversationRepository {
|
interface ConversationRepository {
|
||||||
@ -239,7 +231,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
Log.w("[onversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
|
Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
|
||||||
continuation.resumeWithException(error)
|
continuation.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,9 +322,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
while (reader.next != null) {
|
while (reader.next != null) {
|
||||||
deleteMessageRequest(reader.current)
|
deleteMessageRequest(reader.current)
|
||||||
val recipient = reader.current.recipient
|
val recipient = reader.current.recipient
|
||||||
if (block) {
|
if (block) { setBlocked(recipient, true) }
|
||||||
setBlocked(recipient, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ResultOf.Success(Unit)
|
return ResultOf.Success(Unit)
|
||||||
@ -359,9 +349,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
val cursor = mmsSmsDb.getConversation(threadId, true)
|
val cursor = mmsSmsDb.getConversation(threadId, true)
|
||||||
mmsSmsDb.readerFor(cursor).use { reader ->
|
mmsSmsDb.readerFor(cursor).use { reader ->
|
||||||
while (reader.next != null) {
|
while (reader.next != null) {
|
||||||
if (!reader.current.isOutgoing) {
|
if (!reader.current.isOutgoing) { return true }
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -4,12 +4,8 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.database.MergeCursor;
|
import android.database.MergeCursor;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.messaging.contacts.Contact;
|
import org.session.libsession.messaging.contacts.Contact;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.GroupRecord;
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
@ -27,37 +23,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult;
|
import org.thoughtcrime.securesms.search.model.SearchResult;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
import kotlin.Pair;
|
import kotlin.Pair;
|
||||||
|
|
||||||
/**
|
// Class to manage data retrieval for search
|
||||||
* Manages data retrieval for search.
|
|
||||||
*/
|
|
||||||
public class SearchRepository {
|
public class SearchRepository {
|
||||||
|
|
||||||
private static final String TAG = SearchRepository.class.getSimpleName();
|
private static final String TAG = SearchRepository.class.getSimpleName();
|
||||||
|
|
||||||
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
|
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
|
||||||
static {
|
static {
|
||||||
// Several ranges of invalid ASCII characters
|
// Construct a list containing several ranges of invalid ASCII characters
|
||||||
for (int i = 33; i <= 47; i++) {
|
// See: https://www.ascii-code.com/
|
||||||
BANNED_CHARACTERS.add((char) i);
|
for (int i = 33; i <= 47; i++) { BANNED_CHARACTERS.add((char) i); } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /
|
||||||
}
|
for (int i = 58; i <= 64; i++) { BANNED_CHARACTERS.add((char) i); } // :, ;, <, =, >, ?, @
|
||||||
for (int i = 58; i <= 64; i++) {
|
for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, `
|
||||||
BANNED_CHARACTERS.add((char) i);
|
for (int i = 123; i <= 126; i++) { BANNED_CHARACTERS.add((char) i); } // {, |, }, ~
|
||||||
}
|
|
||||||
for (int i = 91; i <= 96; i++) {
|
|
||||||
BANNED_CHARACTERS.add((char) i);
|
|
||||||
}
|
|
||||||
for (int i = 123; i <= 126; i++) {
|
|
||||||
BANNED_CHARACTERS.add((char) i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -86,35 +70,25 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
||||||
if (TextUtils.isEmpty(query)) {
|
// If the sanitized search is empty then abort without search
|
||||||
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
|
if (cleanQuery.isEmpty()) {
|
||||||
callback.onResult(SearchResult.EMPTY);
|
callback.onResult(SearchResult.EMPTY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
Stopwatch timer = new Stopwatch("FtsQuery");
|
Stopwatch timer = new Stopwatch("FtsQuery");
|
||||||
|
|
||||||
String cleanQuery = sanitizeQuery(query);
|
|
||||||
|
|
||||||
// If the search is for a single character and it was stripped by `sanitizeQuery` then abort
|
|
||||||
// the search for an empty string to avoid SQLite error.
|
|
||||||
if (cleanQuery.length() == 0)
|
|
||||||
{
|
|
||||||
Log.d(TAG, "Aborting empty search query.");
|
|
||||||
timer.stop(TAG);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
timer.split("clean");
|
timer.split("clean");
|
||||||
|
|
||||||
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||||
timer.split("contacts");
|
timer.split("Contacts");
|
||||||
|
|
||||||
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
||||||
timer.split("conversations");
|
timer.split("Conversations");
|
||||||
|
|
||||||
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
CursorList<MessageResult> messages = queryMessages(cleanQuery);
|
||||||
timer.split("messages");
|
timer.split("Messages");
|
||||||
|
|
||||||
timer.stop(TAG);
|
timer.stop(TAG);
|
||||||
|
|
||||||
@ -123,23 +97,20 @@ public class SearchRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
|
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
|
||||||
if (TextUtils.isEmpty(query)) {
|
// If the sanitized search query is empty then abort the search
|
||||||
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
|
if (cleanQuery.isEmpty()) {
|
||||||
callback.onResult(CursorList.emptyList());
|
callback.onResult(CursorList.emptyList());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
// If the sanitized search query is empty then abort the search to prevent SQLite errors.
|
|
||||||
String cleanQuery = sanitizeQuery(query).trim();
|
|
||||||
if (cleanQuery.isEmpty()) { return; }
|
|
||||||
|
|
||||||
CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
|
CursorList<MessageResult> messages = queryMessages(cleanQuery, threadId);
|
||||||
callback.onResult(messages);
|
callback.onResult(messages);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||||
|
|
||||||
Cursor contacts = contactDatabase.queryContactsByName(query);
|
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||||
List<Address> contactList = new ArrayList<>();
|
List<Address> contactList = new ArrayList<>();
|
||||||
List<String> contactStrings = new ArrayList<>();
|
List<String> contactStrings = new ArrayList<>();
|
||||||
@ -166,11 +137,10 @@ public class SearchRepository {
|
|||||||
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
|
MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients});
|
||||||
|
|
||||||
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
|
return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
||||||
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
||||||
String localUserNumber = TextSecurePreferences.getLocalNumber(context);
|
String localUserNumber = TextSecurePreferences.getLocalNumber(context);
|
||||||
if (localUserNumber != null) {
|
if (localUserNumber != null) {
|
||||||
matchingAddresses.remove(localUserNumber);
|
matchingAddresses.remove(localUserNumber);
|
||||||
@ -189,9 +159,7 @@ public class SearchRepository {
|
|||||||
membersGroupList.close();
|
membersGroupList.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
|
Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses));
|
||||||
|
|
||||||
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
|
return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase))
|
||||||
: CursorList.emptyList();
|
: CursorList.emptyList();
|
||||||
}
|
}
|
||||||
@ -256,9 +224,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
RecipientModelBuilder(@NonNull Context context) {
|
RecipientModelBuilder(@NonNull Context context) { this.context = context; }
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Recipient build(@NonNull Cursor cursor) {
|
public Recipient build(@NonNull Cursor cursor) {
|
||||||
@ -301,9 +267,7 @@ public class SearchRepository {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
MessageModelBuilder(@NonNull Context context) {
|
MessageModelBuilder(@NonNull Context context) { this.context = context; }
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageResult build(@NonNull Cursor cursor) {
|
public MessageResult build(@NonNull Cursor cursor) {
|
||||||
|
@ -151,8 +151,8 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
|
|||||||
|
|
||||||
val userPublicKey = getLocalNumber(context)
|
val userPublicKey = getLocalNumber(context)
|
||||||
val senderPublicKey = message.sender
|
val senderPublicKey = message.sender
|
||||||
val sentTimestamp = if (message.sentTimestamp == null) 0 else message.sentTimestamp!!
|
val sentTimestamp = message.sentTimestamp ?: 0
|
||||||
val expireStartedAt = if (expiryMode is AfterSend || message.isSenderSelf) sentTimestamp else 0
|
val expireStartedAt = if ((expiryMode is AfterSend || message.isSenderSelf) && !message.isGroup) sentTimestamp else 0
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (senderPublicKey == null || userPublicKey == senderPublicKey) {
|
if (senderPublicKey == null || userPublicKey == senderPublicKey) {
|
||||||
|
@ -182,9 +182,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
|
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
|
||||||
val intent = Intent(ACTION_WANTS_TO_ANSWER)
|
val intent = Intent(ACTION_WANTS_TO_ANSWER).putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
|
||||||
.putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
|
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,9 +504,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAnswerCall(intent: Intent) {
|
private fun handleAnswerCall(intent: Intent) {
|
||||||
val recipient = callManager.recipient ?: return
|
val recipient = callManager.recipient ?: return Log.e(TAG, "No recipient to answer in handleAnswerCall")
|
||||||
val pending = callManager.pendingOffer ?: return
|
val pending = callManager.pendingOffer ?: return Log.e(TAG, "No pending offer in handleAnswerCall")
|
||||||
val callId = callManager.callId ?: return
|
val callId = callManager.callId ?: return Log.e(TAG, "No callId in handleAnswerCall")
|
||||||
val timestamp = callManager.pendingOfferTime
|
val timestamp = callManager.pendingOfferTime
|
||||||
|
|
||||||
if (callManager.currentConnectionState != CallState.RemoteRing) {
|
if (callManager.currentConnectionState != CallState.RemoteRing) {
|
||||||
@ -526,9 +524,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
insertMissedCall(recipient, true)
|
insertMissedCall(recipient, true)
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
if (didHangup) {
|
if (didHangup) { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callManager.postConnectionEvent(Event.SendAnswer) {
|
callManager.postConnectionEvent(Event.SendAnswer) {
|
||||||
@ -686,7 +682,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
private fun registerPowerButtonReceiver() {
|
private fun registerPowerButtonReceiver() {
|
||||||
if (powerButtonReceiver == null) {
|
if (powerButtonReceiver == null) {
|
||||||
powerButtonReceiver = PowerButtonReceiver()
|
powerButtonReceiver = PowerButtonReceiver()
|
||||||
|
|
||||||
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
|
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -719,7 +714,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleCheckTimeout(intent: Intent) {
|
private fun handleCheckTimeout(intent: Intent) {
|
||||||
val callId = callManager.callId ?: return
|
val callId = callManager.callId ?: return
|
||||||
val callState = callManager.currentConnectionState
|
val callState = callManager.currentConnectionState
|
||||||
@ -746,9 +740,9 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
|
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
|
||||||
// start an intent for the fullscreen
|
// Start an intent for the fullscreen call activity
|
||||||
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
|
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
|
||||||
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT)
|
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
|
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
|
||||||
startActivity(foregroundIntent)
|
startActivity(foregroundIntent)
|
||||||
}
|
}
|
||||||
|
@ -37,12 +37,10 @@ public class Stopwatch {
|
|||||||
for (int i = 1; i < splits.size(); i++) {
|
for (int i = 1; i < splits.size(); i++) {
|
||||||
out.append(splits.get(i).label).append(": ");
|
out.append(splits.get(i).label).append(": ");
|
||||||
out.append(splits.get(i).time - splits.get(i - 1).time);
|
out.append(splits.get(i).time - splits.get(i - 1).time);
|
||||||
out.append(" ");
|
out.append("ms ");
|
||||||
}
|
}
|
||||||
|
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime).append("ms.");
|
||||||
out.append("total: ").append(splits.get(splits.size() - 1).time - startTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, out.toString());
|
Log.d(tag, out.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,13 +9,17 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
import androidx.core.graphics.applyCanvas
|
import androidx.core.graphics.applyCanvas
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun View.contains(point: PointF): Boolean {
|
fun View.contains(point: PointF): Boolean {
|
||||||
@ -32,6 +36,24 @@ val View.hitRect: Rect
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent)
|
fun Context.getAccentColor() = getColorFromAttr(R.attr.colorAccent)
|
||||||
|
|
||||||
|
// Method to grab the appropriate attribute for a message colour.
|
||||||
|
// Note: This is an attribute, NOT a resource Id - see `getColorResourceIdFromAttr` for that.
|
||||||
|
@AttrRes
|
||||||
|
fun getMessageTextColourAttr(messageIsOutgoing: Boolean): Int {
|
||||||
|
return if (messageIsOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get an actual R.id.<SOME_COLOUR> resource Id from an attribute such as R.attr.message_sent_text_color etc.
|
||||||
|
@ColorRes
|
||||||
|
fun getColorResourceIdFromAttr(context: Context, attr: Int): Int {
|
||||||
|
val outTypedValue = TypedValue()
|
||||||
|
val successfullyFoundAttribute = context.theme.resolveAttribute(attr, outTypedValue, true)
|
||||||
|
if (successfullyFoundAttribute) { return outTypedValue.resourceId }
|
||||||
|
|
||||||
|
Log.w("ViewUtils", "Could not find colour attribute $attr in theme - using grey as a safe fallback")
|
||||||
|
return R.color.gray50
|
||||||
|
}
|
||||||
|
|
||||||
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
|
fun View.animateSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int, animationDuration: Long = 250) {
|
||||||
val startSize = resources.getDimension(startSizeID)
|
val startSize = resources.getDimension(startSizeID)
|
||||||
val endSize = resources.getDimension(endSizeID)
|
val endSize = resources.getDimension(endSizeID)
|
||||||
@ -70,7 +92,6 @@ fun View.hideKeyboard() {
|
|||||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
|
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
|
||||||
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
|
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
|
||||||
val scale = size.width / measuredWidth.toFloat()
|
val scale = size.width / measuredWidth.toFloat()
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<solid android:color="?android:textColorPrimary"/>
|
<solid android:color="?android:textColorPrimary"/>
|
||||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<solid android:color="?android:textColorPrimary"/>
|
<solid android:color="?android:textColorPrimary"/>
|
||||||
<corners android:radius="@dimen/medium_button_corner_radius" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:elevation="4dp"
|
android:elevation="4dp">
|
||||||
android:padding="@dimen/medium_spacing">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -21,6 +20,8 @@
|
|||||||
android:id="@+id/dialogDescriptionText"
|
android:id="@+id/dialogDescriptionText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginTop="@dimen/large_spacing"
|
android:layout_marginTop="@dimen/large_spacing"
|
||||||
android:text="@string/dialog_clear_all_data_message"
|
android:text="@string/dialog_clear_all_data_message"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
@ -46,16 +47,15 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
||||||
android:id="@+id/clearAllDataButton"
|
android:id="@+id/clearAllDataButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
|
||||||
android:text="@string/dialog_clear_all_data_clear" />
|
android:text="@string/dialog_clear_all_data_clear" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||||
android:id="@+id/cancelButton"
|
android:id="@+id/cancelButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/cancel" />
|
android:text="@string/cancel" />
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
style="@style/Widget.Session.Button.Dialog.UnimportantText"
|
||||||
android:id="@+id/cancelButton"
|
android:id="@+id/cancelButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/cancel" />
|
android:text="@string/cancel" />
|
||||||
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
style="@style/Widget.Session.Button.Dialog.DestructiveText"
|
||||||
android:id="@+id/sendSeedButton"
|
android:id="@+id/sendSeedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/small_button_height"
|
android:layout_height="@dimen/dialog_button_height"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="@dimen/medium_spacing"
|
android:layout_marginStart="@dimen/medium_spacing"
|
||||||
android:text="@string/dialog_send_seed_send_button_title" />
|
android:text="@string/dialog_send_seed_send_button_title" />
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="@string/MessageRecord_you_disabled_disappearing_messages" />
|
tools:text="You disabled disappearing messages" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/call_view"
|
android:id="@+id/call_view"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<string name="ban">مسدود</string>
|
<string name="ban">مسدود</string>
|
||||||
<string name="save">ذخیره</string>
|
<string name="save">ذخیره</string>
|
||||||
<string name="note_to_self">یادداشت به خود</string>
|
<string name="note_to_self">یادداشت به خود</string>
|
||||||
<string name="version_s">نسخه</string>
|
<string name="version_s">%s نسخه</string>
|
||||||
<!-- AbstractNotificationBuilder -->
|
<!-- AbstractNotificationBuilder -->
|
||||||
<string name="AbstractNotificationBuilder_new_message">پیام جدید</string>
|
<string name="AbstractNotificationBuilder_new_message">پیام جدید</string>
|
||||||
<!-- AlbumThumbnailView -->
|
<!-- AlbumThumbnailView -->
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<dimen name="massive_font_size">50sp</dimen>
|
<dimen name="massive_font_size">50sp</dimen>
|
||||||
|
|
||||||
<!-- Element Sizes -->
|
<!-- Element Sizes -->
|
||||||
|
<dimen name="dialog_button_height">60dp</dimen>
|
||||||
<dimen name="small_button_height">34dp</dimen>
|
<dimen name="small_button_height">34dp</dimen>
|
||||||
<dimen name="medium_button_height">38dp</dimen>
|
<dimen name="medium_button_height">38dp</dimen>
|
||||||
<dimen name="large_button_height">54dp</dimen>
|
<dimen name="large_button_height">54dp</dimen>
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
<item name="android:textAllCaps">false</item>
|
<item name="android:textAllCaps">false</item>
|
||||||
<item name="android:textSize">@dimen/small_font_size</item>
|
<item name="android:textSize">@dimen/small_font_size</item>
|
||||||
<item name="android:textColor">?android:textColorPrimary</item>
|
<item name="android:textColor">?android:textColorPrimary</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Session.Button.Dialog.UnimportantText">
|
<style name="Widget.Session.Button.Dialog.UnimportantText">
|
||||||
|
@ -2,7 +2,7 @@ package org.session.libsession.messaging.mentions
|
|||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
object MentionsManager {
|
object MentionsManager {
|
||||||
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
||||||
|
@ -10,11 +10,11 @@ import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
|
|||||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
|
||||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled
|
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED
|
||||||
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.SCREENSHOT
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.truncateIdForDisplay
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
|
|
||||||
object UpdateMessageBuilder {
|
object UpdateMessageBuilder {
|
||||||
@ -31,47 +31,35 @@ object UpdateMessageBuilder {
|
|||||||
else getSenderName(senderId!!)
|
else getSenderName(senderId!!)
|
||||||
|
|
||||||
return when (updateData) {
|
return when (updateData) {
|
||||||
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) {
|
is UpdateMessageData.Kind.GroupCreation -> {
|
||||||
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
if (isOutgoing) context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||||
} else {
|
else context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||||
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) {
|
is UpdateMessageData.Kind.GroupNameChange -> {
|
||||||
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
if (isOutgoing) context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||||
} else {
|
else context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||||
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
||||||
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||||
if (isOutgoing) {
|
if (isOutgoing) context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
||||||
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
else context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||||
} else {
|
|
||||||
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
// 1st case: you are part of the removed members
|
// 1st case: you are part of the removed members
|
||||||
return if (userPublicKey in updateData.updatedMembers) {
|
return if (userPublicKey in updateData.updatedMembers) {
|
||||||
if (isOutgoing) {
|
if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
|
||||||
context.getString(R.string.MessageRecord_left_group)
|
else context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
|
||||||
} else {
|
|
||||||
context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 2nd case: you are not part of the removed members
|
// 2nd case: you are not part of the removed members
|
||||||
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||||
if (isOutgoing) {
|
if (isOutgoing) context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
||||||
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
else context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
|
||||||
} else {
|
|
||||||
context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) {
|
is UpdateMessageData.Kind.GroupMemberLeft -> {
|
||||||
context.getString(R.string.MessageRecord_left_group)
|
if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
|
||||||
} else {
|
else context.getString(R.string.ConversationItem_group_action_left, senderName)
|
||||||
context.getString(R.string.ConversationItem_group_action_left, senderName)
|
|
||||||
}
|
}
|
||||||
else -> return ""
|
else -> return ""
|
||||||
}
|
}
|
||||||
@ -80,7 +68,7 @@ object UpdateMessageBuilder {
|
|||||||
fun buildExpirationTimerMessage(
|
fun buildExpirationTimerMessage(
|
||||||
context: Context,
|
context: Context,
|
||||||
duration: Long,
|
duration: Long,
|
||||||
recipient: Recipient,
|
isGroup: Boolean,
|
||||||
senderId: String? = null,
|
senderId: String? = null,
|
||||||
isOutgoing: Boolean = false,
|
isOutgoing: Boolean = false,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
@ -89,44 +77,28 @@ object UpdateMessageBuilder {
|
|||||||
if (!isOutgoing && senderId == null) return ""
|
if (!isOutgoing && senderId == null) return ""
|
||||||
val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!)
|
val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!)
|
||||||
return if (duration <= 0) {
|
return if (duration <= 0) {
|
||||||
if (isOutgoing) {
|
if (isOutgoing) context.getString(if (isGroup) R.string.MessageRecord_you_turned_off_disappearing_messages else R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1)
|
||||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
else context.getString(if (isGroup) R.string.MessageRecord_s_turned_off_disappearing_messages else R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1, senderName)
|
||||||
else context.getString(if (recipient.is1on1) R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_you_turned_off_disappearing_messages)
|
|
||||||
} else {
|
|
||||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
|
|
||||||
else context.getString(if (recipient.is1on1) R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_s_turned_off_disappearing_messages, senderName)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
|
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
|
||||||
val action = context.getExpirationTypeDisplayValue(timestamp == expireStarted)
|
val action = context.getExpirationTypeDisplayValue(timestamp >= expireStarted)
|
||||||
if (isOutgoing) {
|
if (isOutgoing) context.getString(
|
||||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)
|
if (isGroup) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1,
|
||||||
else context.getString(
|
time,
|
||||||
if (recipient.is1on1) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s,
|
action
|
||||||
time,
|
) else context.getString(
|
||||||
action
|
if (isGroup) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1,
|
||||||
)
|
senderName,
|
||||||
} else {
|
time,
|
||||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
|
action
|
||||||
else context.getString(
|
)
|
||||||
if (recipient.is1on1) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s,
|
|
||||||
senderName,
|
|
||||||
time,
|
|
||||||
action
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
|
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null) = when (kind) {
|
||||||
val senderName = getSenderName(senderId!!)
|
SCREENSHOT -> R.string.MessageRecord_s_took_a_screenshot
|
||||||
return when (kind) {
|
MEDIA_SAVED -> R.string.MessageRecord_media_saved_by_s
|
||||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
}.let { context.getString(it, getSenderName(senderId!!)) }
|
||||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
|
||||||
DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED ->
|
|
||||||
context.getString(R.string.MessageRecord_media_saved_by_s, senderName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
|
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
|
||||||
when (type) {
|
when (type) {
|
||||||
|
@ -847,7 +847,7 @@ interface TextSecurePreferences {
|
|||||||
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
|
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIntegerPreference(context: Context, key: String, defaultValue: Int): Int {
|
fun getIntegerPreference(context: Context, key: String, defaultValue: Int): Int {
|
||||||
return getDefaultSharedPreferences(context).getInt(key, defaultValue)
|
return getDefaultSharedPreferences(context).getInt(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,10 @@ public class ThemeUtil {
|
|||||||
return getAttributeText(context, R.attr.theme_type, "light").equals("dark");
|
return getAttributeText(context, R.attr.theme_type, "light").equals("dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isLightTheme(@NonNull Context context) {
|
||||||
|
return getAttributeText(context, R.attr.theme_type, "light").equals("light");
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) {
|
public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) {
|
||||||
TypedValue typedValue = new TypedValue();
|
TypedValue typedValue = new TypedValue();
|
||||||
Resources.Theme theme = context.getTheme();
|
Resources.Theme theme = context.getTheme();
|
||||||
|
@ -15,10 +15,6 @@
|
|||||||
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||||
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||||
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||||
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
|
|
||||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
|
|
||||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini l’expiration des messages éphémères à %1$s</string>
|
|
||||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini l’expiration des messages éphémères à %2$s</string>
|
|
||||||
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||||
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||||
<!-- expiration -->
|
<!-- expiration -->
|
||||||
|
@ -15,10 +15,6 @@
|
|||||||
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||||
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||||
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||||
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
|
|
||||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
|
|
||||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini l’expiration des messages éphémères à %1$s</string>
|
|
||||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini l’expiration des messages éphémères à %2$s</string>
|
|
||||||
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||||
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||||
<!-- expiration -->
|
<!-- expiration -->
|
||||||
|
@ -17,17 +17,13 @@
|
|||||||
<string name="MessageRecord_missed_call_from">Missed call from %s</string>
|
<string name="MessageRecord_missed_call_from">Missed call from %s</string>
|
||||||
<string name="MessageRecord_follow_setting">Follow Setting</string>
|
<string name="MessageRecord_follow_setting">Follow Setting</string>
|
||||||
<string name="AccessibilityId_follow_setting">Follow setting</string>
|
<string name="AccessibilityId_follow_setting">Follow setting</string>
|
||||||
<string name="MessageRecord_you_disabled_disappearing_messages">You disabled disappearing messages.</string>
|
|
||||||
<string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string>
|
<string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string>
|
||||||
<string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string>
|
<string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string>
|
||||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
|
|
||||||
<string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string>
|
<string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string>
|
||||||
<string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string>
|
<string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string>
|
||||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
|
|
||||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string>
|
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string>
|
||||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string>
|
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string>
|
||||||
<string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string>
|
<string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string>
|
||||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
|
|
||||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string>
|
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string>
|
||||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string>
|
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string>
|
||||||
<string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string>
|
<string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user