diff --git a/app/build.gradle b/app/build.gradle index 06015ea874..73a4e24d03 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 162 -def canonicalVersionName = "1.10.3" +def canonicalVersionCode = 163 +def canonicalVersionName = "1.10.4" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d8473b849..972ec30be1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -254,6 +254,10 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" /> + { - String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this); - byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey); + ThreadUtils.queue(() -> { + // Don't generate a new profile key here; we do that when the user changes their profile picture + String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this); try { - File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey)); - StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length()); - FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { - TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); - TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); + // Read the file into a byte array + InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[1024]; + while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) { + baos.write(buffer, 0, count); + } + baos.flush(); + byte[] profilePicture = baos.toByteArray(); + // Re-upload it + ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> { + // Update the last profile picture upload date + TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime()); return Unit.INSTANCE; }); } catch (Exception exception) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 40981b5cc7..1bc9e43de0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -108,28 +108,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return null // TODO: Implement } - override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { + override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, - attachmentStream.contentType, - attachmentKey, - Optional.of(Util.toIntExact(attachmentStream.length)), - attachmentStream.preview, - attachmentStream.width, attachmentStream.height, - Optional.fromNullable(uploadResult.digest), - attachmentStream.fileName, - attachmentStream.voiceNote, - attachmentStream.caption, - uploadResult.url); + attachmentStream.contentType, + attachmentKey, + Optional.of(Util.toIntExact(attachmentStream.length)), + attachmentStream.preview, + attachmentStream.width, attachmentStream.height, + Optional.fromNullable(uploadResult.digest), + attachmentStream.fileName, + attachmentStream.voiceNote, + attachmentStream.caption, + uploadResult.url); val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get() database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) } - override fun updateAttachmentAfterUploadFailed(attachmentId: Long) { + override fun handleFailedAttachmentUpload(attachmentId: Long) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return - database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId) + database.handleFailedAttachmentUpload(databaseAttachment.attachmentId) } override fun getMessageID(serverID: Long): Long? { @@ -230,23 +230,24 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? { if (TextUtils.isEmpty(location)) { return null } - if (TextUtils.isEmpty(key)) { return null } - + // `key` can be empty in an open group context (no encryption means no encryption key) return try { - val id: Long = location!!.toLong() - val key: ByteArray = Base64.decode(key!!) - SignalServiceAttachmentPointer(id, - contentType, - key, - Optional.of(Util.toIntExact(size)), - Optional.absent(), - width, - height, - Optional.fromNullable(digest), - Optional.fromNullable(fileName), - isVoiceNote, - Optional.fromNullable(caption), - url) + val id = location!!.toLong() + val key = Base64.decode(key!!) + SignalServiceAttachmentPointer( + id, + contentType, + key, + Optional.of(Util.toIntExact(size)), + Optional.absent(), + width, + height, + Optional.fromNullable(digest), + Optional.fromNullable(fileName), + isVoiceNote, + Optional.fromNullable(caption), + url + ) } catch (e: Exception) { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 92845a3ab0..59fc091455 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -384,6 +384,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel()); } else if (openGroupV2 != null) { PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom()); + if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen") + || openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) { + View openGroupGuidelinesView = findViewById(R.id.open_group_guidelines_view); + openGroupGuidelinesView.setVisibility(View.VISIBLE); + } } View rootView = findViewById(R.id.rootView); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 53ab9de4ed..dae7408bb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -546,7 +546,7 @@ public class ConversationFragment extends Fragment .deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser) .success(l -> { for (MessageRecord messageRecord : messageRecords) { - Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); + Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); if (l.contains(serverID)) { if (messageRecord.isMms()) { DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId()); @@ -569,7 +569,7 @@ public class ConversationFragment extends Fragment .deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer()) .success(l -> { for (MessageRecord messageRecord : messageRecords) { - Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); + Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms()); if (serverID != null && serverID.equals(serverId)) { MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms()); break; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index b69f879993..59b7f52c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -393,7 +393,7 @@ public class AttachmentDatabase extends Database { database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); } - public void updateAttachmentAfterUploadFailed(@NonNull AttachmentId id) { + public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index c9669ae1de..dc30c69bad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -351,7 +351,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server) } - override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean { + override fun isDuplicateMessage(timestamp: Long, sender: String): Boolean { return getReceivedMessageTimestamps().contains(timestamp) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index a9c2025258..26e3280f98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; - import android.app.Application; import android.text.TextUtils; @@ -39,7 +38,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); - private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024; + private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024; private static final String KEY_PROFILE_AVATAR = "profile_avatar"; private static final String KEY_ADDRESS = "address"; @@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { this(new Job.Parameters.Builder() - .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.HOURS.toMillis(1)) - .setMaxAttempts(3) - .build(), + .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.HOURS.toMillis(1)) + .setMaxAttempts(10) + .build(), recipient, profileAvatar); } private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { super(parameters); - this.recipient = recipient; this.profileAvatar = profileAvatar; } @@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType @Override public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar) - .putString(KEY_ADDRESS, recipient.getAddress().serialize()) - .build(); + return new Data.Builder() + .putString(KEY_PROFILE_AVATAR, profileAvatar) + .putString(KEY_ADDRESS, recipient.getAddress().serialize()) + .build(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 3bc33c9746..9382bf83e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -24,7 +24,6 @@ import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import network.loki.messenger.R import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -315,23 +314,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val threadID = thread.threadId val recipient = thread.recipient val threadDB = DatabaseFactory.getThreadDatabase(this) - val dialogMessage: String + val message: String if (recipient.isGroupRecipient) { val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull() if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { - dialogMessage = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." } else { - dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message) + message = resources.getString(R.string.activity_home_leave_group_dialog_message) } } else { - dialogMessage = resources.getString(R.string.activity_home_delete_conversation_dialog_message) + message = resources.getString(R.string.activity_home_delete_conversation_dialog_message) } val dialog = AlertDialog.Builder(this) - dialog.setMessage(dialogMessage) + dialog.setMessage(message) dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) { val context = this@HomeActivity as Context - + // Cancel any outstanding jobs + DatabaseFactory.getSessionJobDatabase(context).cancelPendingMessageSendJobs(threadID) // Send a leave group message if this is an active closed group if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { var isClosedGroup: Boolean @@ -350,34 +350,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), return@launch } } - - withContext(Dispatchers.IO) { - val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) - val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) - //TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager - if (publicChat != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) - apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) - apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) - - OpenGroupAPI.leave(publicChat.channel, publicChat.server) - - ApplicationContext.getInstance(context).publicChatManager - .removeChat(publicChat.server, publicChat.channel) - } else if (openGroupV2 != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server) - apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server) - - ApplicationContext.getInstance(context).publicChatManager - .removeChat(openGroupV2.server, openGroupV2.room) - } else { - threadDB.deleteConversation(threadID) - } - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) + // Delete the conversation + val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + if (v1OpenGroup != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server) + apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server) + apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server) + OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server) + ApplicationContext.getInstance(context).publicChatManager + .removeChat(v1OpenGroup.server, v1OpenGroup.channel) + } else if (v2OpenGroup != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server) + apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server) + ApplicationContext.getInstance(context).publicChatManager + .removeChat(v2OpenGroup.server, v2OpenGroup.room) + } else { + threadDB.deleteConversation(threadID) } - + // Update the badge count + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) // Notify the user val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index f25906c167..bbe7bbe8e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -10,8 +10,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import android.widget.GridLayout import android.widget.Toast -import androidx.activity.viewModels import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.view.isVisible import androidx.fragment.app.* @@ -42,9 +42,6 @@ import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel import org.thoughtcrime.securesms.loki.viewmodel.State class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { - - private val viewModel by viewModels() - private val adapter = JoinPublicChatActivityAdapter(this) // region Lifecycle @@ -83,23 +80,18 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode } fun joinPublicChatIfPossible(url: String) { - // add http if just an IP style / host style URL is entered but leave it if scheme is included - val properString = if (!url.startsWith("http")) "http://$url" else url - val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() - - val room = httpUrl.pathSegments().firstOrNull() - val publicKey = httpUrl.queryParameter("public_key") + // Add "http" if not entered explicitly + val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url + val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() + val room = url.pathSegments().firstOrNull() + val publicKey = url.queryParameter("public_key") val isV2OpenGroup = !room.isNullOrEmpty() showLoader() - lifecycleScope.launch(Dispatchers.IO) { try { val (threadID, groupID) = if (isV2OpenGroup) { - val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply { - if (httpUrl.port() != 80 || httpUrl.port() != 443) { - // non-standard port, add to server - this.port(httpUrl.port()) - } + val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply { + if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server }.build() val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) @@ -107,21 +99,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode threadID to groupID } else { val channel: Long = 1 - val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel) + val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, stringWithExplicitScheme, channel) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray()) threadID to groupID } MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) - withContext(Dispatchers.Main) { - // go to the new conversation and finish this one - openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)) + val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false) + openConversationActivity(this@JoinPublicChatActivity, threadID, recipient) finish() } - } catch (e: Exception) { - Log.e("JoinPublicChatActivity", "Failed to join open group.", e) + Log.e("Loki", "Couldn't join open group.", e) withContext(Dispatchers.Main) { hideLoader() Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() @@ -175,19 +165,40 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity // region Enter Chat URL Fragment class EnterChatURLFragment : Fragment() { - private val viewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard + joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } + viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> + defaultRoomsContainer.isVisible = state is State.Success + defaultRoomsLoader.isVisible = state is State.Loading + when (state) { + State.Loading -> { + // TODO: Show a loader + } + is State.Error -> { + // TODO: Hide the loader + } + is State.Success -> { + populateDefaultGroups(state.value) + } + } + } + } + private fun populateDefaultGroups(groups: List) { defaultRoomsGridLayout.removeAllViews() + defaultRoomsGridLayout.useDefaultMargins = false groups.forEach { defaultGroup -> - val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip + val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> - val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size) + val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) RoundedBitmapDrawableFactory.create(resources,bitmap).apply { isCircular = true } @@ -197,35 +208,14 @@ class EnterChatURLFragment : Fragment() { chip.setOnClickListener { (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } + defaultRoomsGridLayout.addView(chip) } - if (groups.size and 1 != 0) { - // add a filler weight 1 view + if ((groups.size and 1) != 0) { // This checks that the number of rooms is even layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard - joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } - viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> - defaultRoomsParent.isVisible = state is State.Success - defaultRoomsLoader.isVisible = state is State.Loading - when (state) { - State.Loading -> { - // show a loader here probs - } - is State.Error -> { - // hide the loader and the - } - is State.Success -> { - populateDefaultGroups(state.value) - } - } - } - } - // region Convenience private fun joinPublicChatIfPossible() { val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt new file mode 100644 index 0000000000..2a9ebe5491 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.loki.activities + +import android.os.Bundle +import kotlinx.android.synthetic.main.activity_open_group_guidelines.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.BaseActionBarActivity + +class OpenGroupGuidelinesActivity : BaseActionBarActivity() { + + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_open_group_guidelines) + communityGuidelinesTextView.text = """ + In order for our open group to be a fun environment, full of robust and constructive discussion, please follow these four simple rules: + + 1. Keep conversations on-topic and add value to the discussion (no referral links, spamming, or off-topic discussion). + + 2. You don't have to love everyone, but be civil (no baiting, excessively partisan arguments, threats, and so on; use common sense). + + 3. Do not be a shill. Comparison and criticism is reasonable, but blatant shilling is not. + + 4. Don't post explicit content, be it excessive offensive language, or content which is sexual or violent in nature. + + If you break these rules, you’ll be warned by an admin. If your behaviour doesn’t improve, you will be removed from the open group. + + If you see or experience any destructive behaviour, please contact an admin. + + —————————— + + SCAMMER WARNING + + Trust only those with an admin tag in the chat. No admin will ever DM you first. No admin will ever message you for Oxen coins. + """.trimIndent() + } + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index c046fd9115..6c6d95511f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -23,18 +23,15 @@ import network.loki.messenger.BuildConfig import network.loki.messenger.R import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.alwaysUi +import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.avatars.AvatarHelper -import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.threads.Address +import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.preferences.ProfileKeyUtil -import org.session.libsignal.service.api.util.StreamDetails import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection @@ -51,7 +48,6 @@ import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil -import java.io.ByteArrayInputStream import java.io.File import java.security.SecureRandom import java.util.* @@ -127,7 +123,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { AvatarSelection.REQUEST_CODE_AVATAR -> { - if (resultCode != Activity.RESULT_OK) { return } + if (resultCode != Activity.RESULT_OK) { + return + } val outputFile = Uri.fromFile(File(cacheDir, "cropped")) var inputFile: Uri? = data?.data if (inputFile == null && tempFile != null) { @@ -136,7 +134,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar) } AvatarSelection.REQUEST_CODE_CROP_IMAGE -> { - if (resultCode != Activity.RESULT_OK) { return } + if (resultCode != Activity.RESULT_OK) { + return + } AsyncTask.execute { try { profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap @@ -186,42 +186,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } val profilePicture = profilePictureToBeUploaded val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) - val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey) if (isUpdatingProfilePicture && profilePicture != null) { - val storageAPI = FileServerAPI.shared - val deferred = deferred() - AsyncTask.execute { - val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong()) - val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) { - TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time) - } - TextSecurePreferences.setProfilePictureURL(this, url) - deferred.resolve(Unit) - } - promises.add(deferred.promise) + promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) } - - all(promises).bind { - // updating the profile name or picture - if (profilePicture != null || displayName != null) { - task { - if (isUpdatingProfilePicture && profilePicture != null) { - AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) - TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) - ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() - } - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) - } - } else { - Promise.of(Unit) + val compoundPromise = all(promises) + compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below + if (isUpdatingProfilePicture && profilePicture != null) { + AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) + TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) + TextSecurePreferences.setLastProfilePictureUpload(this, Date().time) + ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) + ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() } - }.alwaysUi { + if (profilePicture != null || displayName != null) { + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) + } + } + compoundPromise.alwaysUi { if (displayName != null) { btnGroupNameDisplay.text = displayName } if (isUpdatingProfilePicture && profilePicture != null) { - profilePictureView.recycle() // clear cached image before update tje profilePictureView + profilePictureView.recycle() // Clear the cached image before updating profilePictureView.update() } displayNameToBeUploaded = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index 22878d46fd..c270797c6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -11,6 +11,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.Util +import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.groups.GroupManager @@ -138,9 +139,10 @@ class PublicChatManager(private val context: Context) { val groupId = OpenGroup.getId(channel, server) val threadId = GroupManager.getOpenGroupThreadID(groupId, context) val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize() - GroupManager.deleteGroup(groupAddress, context) - - Util.runOnMain { startPollersIfNeeded() } + ThreadUtils.queue { + GroupManager.deleteGroup(groupAddress, context) // Must be invoked on a background thread + Util.runOnMain { startPollersIfNeeded() } + } } fun removeChat(server: String, room: String) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt index 85a66b159c..83dc565117 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context +import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -98,11 +99,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) { val database = databaseHelper.writableDatabase - val contentValues = ContentValues(2) + val contentValues = ContentValues(3) contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.serverID, serverID) contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE) - database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString())) + database.insertWithOnConflict(messageIDTable, null, contentValues, CONFLICT_REPLACE) } fun getOriginalThreadID(messageID: Long): Long { @@ -114,11 +115,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) { val database = databaseHelper.writableDatabase - val contentValues = ContentValues(2) + val contentValues = ContentValues(3) contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.serverID, serverID) contentValues.put(Companion.threadID, threadID) - database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString())) + database.insertWithOnConflict(messageThreadMappingTable, null, contentValues, CONFLICT_REPLACE) } fun getErrorMessage(messageID: Long): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt index 22c8d48f44..c5e5e102ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt @@ -71,6 +71,30 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } + fun cancelPendingMessageSendJobs(threadID: Long) { + val database = databaseHelper.writableDatabase + val attachmentUploadJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as AttachmentUploadJob? + if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) } + } + val messageSendJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( MessageSendJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as MessageSendJob? + if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) } + } + if (attachmentUploadJobKeys.isNotEmpty()) { + val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString )) + } + if (messageSendJobKeys.isNotEmpty()) { + val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString )) + } + } + fun isJobCanceled(job: Job): Boolean { val database = databaseHelper.readableDatabase var cursor: android.database.Cursor? = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 5f61506504..745e9067f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString +import org.session.libsession.messaging.sending_receiving.* import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.ECKeyPair @@ -15,12 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation -import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase -import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair -import org.session.libsession.messaging.sending_receiving.pendingKeyPair -import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.GroupRecord @@ -195,7 +191,7 @@ object ClosedGroupsProtocolV2 { } if (userPublicKey in admins) { // send current encryption key to the latest added members - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull() ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") @@ -330,7 +326,7 @@ object ClosedGroupsProtocolV2 { // Find our wrapper and decrypt it if possible val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray() - val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first + val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first // Parse it val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt index cf8c747638..66d7263747 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt @@ -35,11 +35,10 @@ object OpenGroupUtilities { val groupInfo = OpenGroupAPIV2.getInfo(room,server).get() val application = ApplicationContext.getInstance(context) - val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey) - val storage = MessagingModuleConfiguration.shared.storage storage.removeLastDeletionServerId(room, server) storage.removeLastMessageServerId(room, server) + val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey) return group } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt index a2d747ed9b..d4281a6cfb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt @@ -20,5 +20,4 @@ class DefaultGroupsViewModel : ViewModel() { }.onStart { emit(State.Loading) }.asLiveData() - } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt new file mode 100644 index 0000000000..d29460866c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.loki.views + +import android.content.Context +import android.content.Intent +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import kotlinx.android.synthetic.main.view_open_group_guidelines.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity +import org.thoughtcrime.securesms.loki.utilities.push + +class OpenGroupGuidelinesView : FrameLayout { + + constructor(context: Context) : super(context) { + setUpViewHierarchy() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + setUpViewHierarchy() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + setUpViewHierarchy() + } + + private fun setUpViewHierarchy() { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null) + addView(contentView) + readButton.setOnClickListener { + val activity = context as ConversationActivity + val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java) + activity.push(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml index e31e269a2e..fb1fc1c6ca 100644 --- a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml +++ b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index a4b088aac1..a011b2bf4b 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -33,23 +33,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pn_mode.xml b/app/src/main/res/layout/activity_pn_mode.xml index 2e8f12b7f2..b86c0a3221 100644 --- a/app/src/main/res/layout/activity_pn_mode.xml +++ b/app/src/main/res/layout/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml index 0b9ff3d077..ba9f7f27e1 100644 --- a/app/src/main/res/layout/conversation_activity.xml +++ b/app/src/main/res/layout/conversation_activity.xml @@ -134,6 +134,12 @@ android:background="?android:dividerHorizontal" android:elevation="1dp" /> + + - \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_enter_chat_url.xml b/app/src/main/res/layout/fragment_enter_chat_url.xml index 7affed157e..462c99e434 100644 --- a/app/src/main/res/layout/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout/fragment_enter_chat_url.xml @@ -33,23 +33,27 @@ + + + + + + + + + + + + + + + + + + + +