This commit is contained in:
Ryan Zhao 2021-07-08 09:25:48 +10:00
commit 69b05afbd9
252 changed files with 2921 additions and 11255 deletions

View File

@ -143,8 +143,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 188 def canonicalVersionCode = 193
def canonicalVersionName = "1.11.0" def canonicalVersionName = "1.11.2"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -194,8 +194,8 @@ android {
versionCode canonicalVersionCode * postFixSize versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName versionName canonicalVersionName
minSdkVersion 23 minSdkVersion androidMinimumSdkVersion
targetSdkVersion 30 targetSdkVersion androidCompileSdkVersion
multiDexEnabled = true multiDexEnabled = true

View File

@ -207,21 +207,14 @@
</activity-alias> </activity-alias>
<activity <activity
android:name="org.thoughtcrime.securesms.conversation.ConversationActivity" android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:launchMode="singleTask"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
android:parentActivityName="org.thoughtcrime.securesms.loki.activities.HomeActivity" android:parentActivityName="org.thoughtcrime.securesms.loki.activities.HomeActivity"
android:windowSoftInputMode="stateUnchanged"> android:theme="@style/Theme.Session.DayNight.FlatActionBar">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" /> android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
</activity> </activity>
<activity
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity <activity
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity" android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@ -230,21 +223,6 @@
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity" android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight" /> android:theme="@style/Theme.TextSecure.DayNight" />
<activity
android:name="org.thoughtcrime.securesms.conversation.ConversationPopupActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:windowSoftInputMode="stateVisible" />
<activity
android:name="org.thoughtcrime.securesms.MessageDetailsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:label="Message Details"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight"
android:launchMode="singleTask"
android:windowSoftInputMode="stateHidden" />
<activity <activity
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity" android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"

View File

@ -1,476 +0,0 @@
/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import org.session.libsession.messaging.messages.visible.LinkPreview;
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
import org.session.libsession.messaging.messages.visible.Quote;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.open_groups.OpenGroupV2;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.utilities.UpdateMessageData;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.session.libsession.utilities.MaterialColor;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.guava.Optional;
import java.lang.ref.WeakReference;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import network.loki.messenger.R;
/**
* @author Jake McGinty
*/
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor>, RecipientModifiedListener {
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
public final static String MESSAGE_ID_EXTRA = "message_id";
public final static String THREAD_ID_EXTRA = "thread_id";
public final static String IS_PUSH_GROUP_EXTRA = "is_push_group";
public final static String TYPE_EXTRA = "type";
public final static String ADDRESS_EXTRA = "address";
private GlideRequests glideRequests;
private long threadId;
private boolean isPushGroup;
private ConversationItem conversationItem;
private ViewGroup itemParent;
private View metadataContainer;
private View expiresContainer;
private TextView errorText;
private View resendButton;
private TextView sentDate;
private TextView receivedDate;
private TextView expiresInText;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
private View separator;
private ListView recipientsList;
private LayoutInflater inflater;
private boolean running;
@Override
public void onCreate(Bundle bundle, boolean ready) {
super.onCreate(bundle, ready);
setContentView(R.layout.message_details_activity);
running = true;
initializeResources();
initializeActionBar();
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onResume() {
super.onResume();
assert getSupportActionBar() != null;
getSupportActionBar().setTitle("Message Details");
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId);
}
@Override
protected void onPause() {
super.onPause();
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1L);
}
@Override
protected void onDestroy() {
super.onDestroy();
running = false;
}
private void initializeActionBar() {
assert getSupportActionBar() != null;
Recipient recipient = Recipient.from(this, getIntent().getParcelableExtra(ADDRESS_EXTRA), true);
recipient.addListener(this);
}
private void setActionBarColor(MaterialColor color) {
assert getSupportActionBar() != null;
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
}
@Override
public void onModified(final Recipient recipient) {
Util.runOnMain(() -> setActionBarColor(recipient.getColor()));
}
private void initializeResources() {
inflater = LayoutInflater.from(this);
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
glideRequests = GlideApp.with(this);
itemParent = header.findViewById(R.id.item_container);
recipientsList = findViewById(R.id.recipients_list);
metadataContainer = header.findViewById(R.id.metadata_container);
errorText = header.findViewById(R.id.error_text);
resendButton = header.findViewById(R.id.resend_button);
sentDate = header.findViewById(R.id.sent_time);
receivedContainer = header.findViewById(R.id.received_container);
receivedDate = header.findViewById(R.id.received_time);
transport = header.findViewById(R.id.transport);
toFrom = header.findViewById(R.id.tofrom);
separator = header.findViewById(R.id.separator);
expiresContainer = header.findViewById(R.id.expires_container);
expiresInText = header.findViewById(R.id.expires_in);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
private void updateTransport(MessageRecord messageRecord) {
final String transportText;
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = getString(R.string.ConversationFragment_pending);
} else if (messageRecord.isMms()) {
transportText = getString(R.string.ConversationFragment_mms);
} else {
transportText = getString(R.string.ConversationFragment_sms);
}
transport.setText(transportText);
}
private void updateTime(MessageRecord messageRecord) {
sentDate.setOnLongClickListener(null);
receivedDate.setOnLongClickListener(null);
if (messageRecord.isPending() || messageRecord.isFailed()) {
sentDate.setText("-");
receivedContainer.setVisibility(View.GONE);
} else {
Locale dateLocale = Locale.getDefault();
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
sentDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
return true;
});
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
receivedDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
return true;
});
receivedContainer.setVisibility(View.VISIBLE);
} else {
receivedContainer.setVisibility(View.GONE);
}
}
}
private void updateExpirationTime(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
expiresContainer.setVisibility(View.GONE);
return;
}
expiresContainer.setVisibility(View.VISIBLE);
Util.runOnMain(new Runnable() {
@Override
public void run() {
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
long remaining = messageRecord.getExpiresIn() - elapsed;
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
expiresInText.setText(duration);
if (running) {
Util.runOnMainDelayed(this, 500);
}
}
});
}
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
final int toFromRes;
if (messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__to;
} else {
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
long threadID = messageRecord.getThreadId();
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID);
if (openGroup != null && messageRecord.isOutgoing()) {
toFrom.setVisibility(View.GONE);
separator.setVisibility(View.GONE);
}
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), recipient, null, false);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
}
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
if (conversationItem == null) {
if (messageRecord.isOutgoing()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
} else {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
}
itemParent.addView(conversationItem);
}
}
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
return reader.getNext();
case MmsSmsDatabase.MMS_TRANSPORT:
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
return mmsReader.getNext();
default:
throw new AssertionError("no valid message type specified");
}
}
private void copyToClipboard(@NonNull String text) {
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
if (messageRecord == null) {
finish();
} else {
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
recipientsList.setAdapter(null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
@SuppressLint("StaticFieldLeak")
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
private final WeakReference<Context> weakContext;
private final MessageRecord messageRecord;
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
protected Context getContext() {
return weakContext.get();
}
@Override
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
Context context = getContext();
if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early");
return null;
}
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroupRecipient()) {
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), true, -1));
} else {
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().getAddress().toGroupString(), false);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
}
} else {
for (GroupReceiptInfo info : receiptInfoList) {
recipients.add(new RecipientDeliveryStatus(Recipient.from(context, info.getAddress(), true),
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
info.isUnidentified(),
info.getTimestamp()));
}
}
}
return recipients;
}
@Override
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return;
}
inflateMessageViewIfAbsent(messageRecord);
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup;
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(getContext());
String errorMessage = lokiMessageDatabase.getErrorMessage(messageRecord.id);
if (errorMessage != null) {
errorText.setText(errorMessage);
}
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.VISIBLE);
resendButton.setOnClickListener(this::onResendClicked);
metadataContainer.setVisibility(View.GONE);
} else if (messageRecord.isFailed()) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.GONE);
} else {
updateTransport(messageRecord);
updateTime(messageRecord);
updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.VISIBLE);
}
}
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
else return RecipientDeliveryStatus.Status.PENDING;
}
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
throw new AssertionError();
}
private void onResendClicked(View v) {
Recipient recipient = messageRecord.getRecipient();
VisibleMessage message = new VisibleMessage();
message.setId(messageRecord.getId());
if (messageRecord.isOpenGroupInvitation()) {
OpenGroupInvitation openGroupInvitation = new OpenGroupInvitation();
UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(messageRecord.getBody());
if (updateMessageData.getKind() instanceof UpdateMessageData.Kind.OpenGroupInvitation) {
UpdateMessageData.Kind.OpenGroupInvitation data = (UpdateMessageData.Kind.OpenGroupInvitation)updateMessageData.getKind();
openGroupInvitation.setName(data.getGroupName());
openGroupInvitation.setUrl(data.getGroupUrl());
}
message.setOpenGroupInvitation(openGroupInvitation);
} else {
message.setText(messageRecord.getBody());
}
message.setSentTimestamp(messageRecord.getTimestamp());
if (recipient.isGroupRecipient()) {
message.setGroupPublicKey(recipient.getAddress().toGroupString());
} else {
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
}
message.setThreadID(messageRecord.getThreadId());
if (messageRecord.isMms()) {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
}
if (mmsMessageRecord.getQuote() != null) {
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
}
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
}
MessageSender.send(message, recipient.getAddress());
resendButton.setVisibility(View.GONE);
}
}
}

View File

@ -37,7 +37,7 @@ import androidx.appcompat.widget.Toolbar;
import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.utilities.DistributionTypes;
import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -219,7 +219,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
final Intent intent = getBaseShareIntent(ConversationActivityV2.class); final Intent intent = getBaseShareIntent(ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, address); intent.putExtra(ConversationActivityV2.ADDRESS, address);
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
isPassingAlongMedia = true; isPassingAlongMedia = true;
startActivity(intent); startActivity(intent);
@ -227,11 +226,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private Intent getBaseShareIntent(final @NonNull Class<?> target) { private Intent getBaseShareIntent(final @NonNull Class<?> target) {
final Intent intent = new Intent(this, target); final Intent intent = new Intent(this, target);
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
final ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType); if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);

View File

@ -9,12 +9,12 @@ import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.UploadResult import org.session.libsession.utilities.UploadResult
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.messages.SignalServiceAttachment import org.session.libsignal.messages.SignalServiceAttachment
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceAttachmentStream import org.session.libsignal.messages.SignalServiceAttachmentStream
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -97,6 +97,19 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream) attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream)
} }
override fun updateAudioAttachmentDuration(
attachmentId: AttachmentId,
durationMs: Long,
threadId: Long
) {
val attachmentDb = DatabaseFactory.getAttachmentDatabase(context)
attachmentDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras(
attachmentId = attachmentId,
visualSamples = byteArrayOf(),
durationMs = durationMs
), threadId)
}
override fun isOutgoingMessage(timestamp: Long): Boolean { override fun isOutgoingMessage(timestamp: Long): Boolean {
val smsDatabase = DatabaseFactory.getSmsDatabase(context) val smsDatabase = DatabaseFactory.getSmsDatabase(context)
val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val mmsDatabase = DatabaseFactory.getMmsDatabase(context)

View File

@ -1,88 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import network.loki.messenger.R;
/**
* Bottom navigation bar shown in the {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
* when the user is searching within a conversation. Shows details about the results and allows the
* user to move between them.
*/
public class ConversationSearchBottomBar extends ConstraintLayout {
private View searchDown;
private View searchUp;
private TextView searchPositionText;
private View progressWheel;
private EventListener eventListener;
public ConversationSearchBottomBar(Context context) {
super(context);
}
public ConversationSearchBottomBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.searchUp = findViewById(R.id.conversation_search_up);
this.searchDown = findViewById(R.id.conversation_search_down);
this.searchPositionText = findViewById(R.id.conversation_search_position);
this.progressWheel = findViewById(R.id.conversation_search_progress_wheel);
}
public void setData(int position, int count) {
progressWheel.setVisibility(GONE);
searchUp.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onSearchMoveUpPressed();
}
});
searchDown.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onSearchMoveDownPressed();
}
});
if (count > 0) {
searchPositionText.setText(getResources().getString(R.string.ConversationActivity_search_position, position + 1, count));
} else {
searchPositionText.setText(R.string.ConversationActivity_no_results);
}
setViewEnabled(searchUp, position < (count - 1));
setViewEnabled(searchDown, position > 0);
}
public void showLoading() {
progressWheel.setVisibility(VISIBLE);
}
private void setViewEnabled(@NonNull View view, boolean enabled) {
view.setEnabled(enabled);
view.setAlpha(enabled ? 1f : 0.25f);
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public interface EventListener {
void onSearchMoveUpPressed();
void onSearchMoveDownPressed();
}
}

View File

@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.PorterDuff;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.ThemeUtil;
import java.util.List;
import network.loki.messenger.R;
public class ConversationTypingView extends LinearLayout {
private AvatarImageView avatar;
private View bubble;
private TypingIndicatorView indicator;
public ConversationTypingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.typing_avatar);
bubble = findViewById(R.id.typing_bubble);
indicator = findViewById(R.id.typing_indicator);
}
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread) {
if (typists.isEmpty()) {
indicator.stopAnimation();
return;
}
Recipient typist = typists.get(0);
bubble.getBackground().setColorFilter(
ThemeUtil.getThemedColor(getContext(), R.attr.message_received_background_color),
PorterDuff.Mode.MULTIPLY);
if (isGroupThread) {
avatar.setAvatar(glideRequests, typist, false);
avatar.setVisibility(VISIBLE);
} else {
avatar.setVisibility(GONE);
}
indicator.startAnimation();
}
}

View File

@ -4,73 +4,11 @@ import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.annotation.DimenRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import android.text.format.DateUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.Interpolator;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; public class InputPanel extends LinearLayout {
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.SettableFuture;
import org.session.libsignal.utilities.guava.Optional;
import java.util.concurrent.TimeUnit;
import network.loki.messenger.R;
public class InputPanel extends LinearLayout
implements MicrophoneRecorderView.Listener,
KeyboardAwareLinearLayout.OnKeyboardShownListener,
EmojiKeyboardProvider.EmojiEventListener
{
private static final String TAG = InputPanel.class.getSimpleName();
private static final int FADE_TIME = 150;
private QuoteView quoteView;
private LinkPreviewView linkPreview;
private EmojiToggle mediaKeyboard;
public ComposeText composeText;
private View quickCameraToggle;
private View quickAudioToggle;
private View buttonToggle;
private View recordingContainer;
private View recordLockCancel;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private @Nullable Listener listener;
private boolean emojiVisible;
public InputPanel(Context context) { public InputPanel(Context context) {
super(context); super(context);
@ -85,361 +23,6 @@ public class InputPanel extends LinearLayout
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
} }
@Override
public void onFinishInflate() {
super.onFinishInflate();
View quoteDismiss = findViewById(R.id.quote_dismiss);
this.quoteView = findViewById(R.id.quote_view);
this.linkPreview = findViewById(R.id.link_preview);
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
this.composeText = findViewById(R.id.embedded_text_editor);
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
this.buttonToggle = findViewById(R.id.button_toggle);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordLockCancel = findViewById(R.id.record_cancel);
View slideToCancelView = findViewById(R.id.slide_to_cancel);
this.slideToCancel = new SlideToCancel(slideToCancelView);
this.microphoneRecorderView = findViewById(R.id.recorder_view);
this.microphoneRecorderView.setListener(this);
this.recordTime = new RecordTime(findViewById(R.id.record_time),
findViewById(R.id.microphone),
TimeUnit.HOURS.toSeconds(1),
() -> microphoneRecorderView.cancelAction());
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
mediaKeyboard.setVisibility(View.GONE);
emojiVisible = false;
} else {
mediaKeyboard.setVisibility(View.VISIBLE);
emojiVisible = true;
}
quoteDismiss.setOnClickListener(v -> clearQuote());
linkPreview.setCloseClickedListener(() -> {
if (listener != null) {
listener.onLinkPreviewCanceled();
}
});
}
public void setListener(final @NonNull Listener listener) {
this.listener = listener;
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
}
public void setMediaListener(@NonNull MediaListener listener) {
composeText.setMediaListener(listener);
}
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments, @NonNull Recipient conversationRecipient, long threadID) {
this.quoteView.setQuote(glideRequests, id, author, MentionUtilities.highlightMentions(body, threadID, getContext()), false, attachments, conversationRecipient);
this.quoteView.setVisibility(View.VISIBLE);
if (this.linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
}
public void clearQuote() {
this.quoteView.dismiss();
if (this.linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_radius);
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
}
public Optional<QuoteModel> getQuote() {
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getAddress(), quoteView.getBody(), false, quoteView.getAttachments()));
} else {
return Optional.absent();
}
}
public void setLinkPreviewLoading() {
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLoading();
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
if (preview.isPresent()) {
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLinkPreview(glideRequests, preview.get(), true);
} else {
this.linkPreview.setVisibility(View.GONE);
}
int largeCornerRadius = (int)(16 * getResources().getDisplayMetrics().density);
int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius) : largeCornerRadius;
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
this.mediaKeyboard.attach(mediaKeyboard);
}
@Override
public void onRecordPermissionRequired() {
if (listener != null) listener.onRecorderPermissionRequired();
}
@Override
public void onRecordPressed() {
if (listener != null) listener.onRecorderStarted();
recordTime.display();
slideToCancel.display();
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start();
}
@Override
public void onRecordReleased() {
long elapsedTime = onRecordHideEvent();
if (listener != null) {
Log.d(TAG, "Elapsed time: " + elapsedTime);
if (elapsedTime > 1000) {
listener.onRecorderFinished();
} else {
Toast.makeText(getContext(), R.string.InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send, Toast.LENGTH_LONG).show();
listener.onRecorderCanceled();
}
}
}
@Override
public void onRecordMoved(float offsetX, float absoluteX) {
slideToCancel.moveTo(offsetX);
int direction = ViewCompat.getLayoutDirection(this);
float position = absoluteX / recordingContainer.getWidth();
if (direction == ViewCompat.LAYOUT_DIRECTION_LTR && position <= 0.5 ||
direction == ViewCompat.LAYOUT_DIRECTION_RTL && position >= 0.6)
{
this.microphoneRecorderView.cancelAction();
}
}
@Override
public void onRecordCanceled() {
onRecordHideEvent();
if (listener != null) listener.onRecorderCanceled();
}
@Override
public void onRecordLocked() {
slideToCancel.hide();
recordLockCancel.setVisibility(View.VISIBLE);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
if (listener != null) listener.onRecorderLocked();
}
public void onPause() {
this.microphoneRecorderView.cancelAction();
}
public void setEnabled(boolean enabled) {
composeText.setEnabled(enabled);
mediaKeyboard.setEnabled(enabled);
quickAudioToggle.setEnabled(enabled);
quickCameraToggle.setEnabled(enabled);
}
public void setHint(@NonNull String hint) {
composeText.setHint(hint, null);
}
private long onRecordHideEvent() {
recordLockCancel.setVisibility(View.GONE);
ListenableFuture<Void> future = slideToCancel.hide();
long elapsedTime = recordTime.hide();
future.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
}
});
return elapsedTime;
}
@Override
public void onKeyboardShown() {
mediaKeyboard.setToMedia();
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
composeText.dispatchKeyEvent(keyEvent);
}
@Override
public void onEmojiSelected(String emoji) {
composeText.insertEmoji(emoji);
}
private int readDimen(@DimenRes int dimenRes) {
return getResources().getDimensionPixelSize(dimenRes);
}
public boolean isRecordingInLockedMode() {
return microphoneRecorderView.isRecordingLocked();
}
public void releaseRecordingLock() {
microphoneRecorderView.unlockAction();
}
public interface Listener {
void onRecorderStarted();
void onRecorderLocked();
void onRecorderFinished();
void onRecorderCanceled();
void onRecorderPermissionRequired();
void onEmojiToggle();
void onLinkPreviewCanceled();
}
private static class SlideToCancel {
private final View slideToCancelView;
SlideToCancel(View slideToCancelView) {
this.slideToCancelView = slideToCancelView;
}
public void display() {
ViewUtil.fadeIn(this.slideToCancelView, FADE_TIME);
}
public ListenableFuture<Void> hide() {
final SettableFuture<Void> future = new SettableFuture<>();
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, slideToCancelView.getTranslationX(),
Animation.ABSOLUTE, 0,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0));
animation.addAnimation(new AlphaAnimation(1, 0));
animation.setDuration(MicrophoneRecorderView.ANIMATION_DURATION);
animation.setFillBefore(true);
animation.setFillAfter(false);
slideToCancelView.postDelayed(() -> future.set(null), MicrophoneRecorderView.ANIMATION_DURATION);
slideToCancelView.setVisibility(View.GONE);
slideToCancelView.startAnimation(animation);
return future;
}
void moveTo(float offset) {
Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation.ABSOLUTE, offset,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
animation.setDuration(0);
animation.setFillAfter(true);
animation.setFillBefore(true);
slideToCancelView.startAnimation(animation);
}
}
private static class RecordTime implements Runnable {
private final @NonNull TextView recordTimeView;
private final @NonNull View microphone;
private final @NonNull Runnable onLimitHit;
private final long limitSeconds;
private long startTime;
private RecordTime(@NonNull TextView recordTimeView, @NonNull View microphone, long limitSeconds, @NonNull Runnable onLimitHit) {
this.recordTimeView = recordTimeView;
this.microphone = microphone;
this.limitSeconds = limitSeconds;
this.onLimitHit = onLimitHit;
}
@MainThread
public void display() {
this.startTime = System.currentTimeMillis();
this.recordTimeView.setText(DateUtils.formatElapsedTime(0));
ViewUtil.fadeIn(this.recordTimeView, FADE_TIME);
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
microphone.setVisibility(View.VISIBLE);
microphone.startAnimation(pulseAnimation());
}
@MainThread
public long hide() {
long elapsedTime = System.currentTimeMillis() - startTime;
this.startTime = 0;
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
microphone.clearAnimation();
ViewUtil.fadeOut(this.microphone, FADE_TIME, View.INVISIBLE);
return elapsedTime;
}
@Override
@MainThread
public void run() {
long localStartTime = startTime;
if (localStartTime > 0) {
long elapsedTime = System.currentTimeMillis() - localStartTime;
long elapsedSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTime);
if (elapsedSeconds >= limitSeconds) {
onLimitHit.run();
} else {
recordTimeView.setText(DateUtils.formatElapsedTime(elapsedSeconds));
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
}
}
}
private static Animation pulseAnimation() {
AlphaAnimation animation = new AlphaAnimation(0, 1);
animation.setInterpolator(pulseInterpolator());
animation.setRepeatCount(Animation.INFINITE);
animation.setDuration(1000);
return animation;
}
private static Interpolator pulseInterpolator() {
return input -> {
input *= 5;
if (input > 1) {
input = 4 - input;
}
return Math.max(0, Math.min(1, input));
};
}
}
public interface MediaListener { public interface MediaListener {
void onMediaSelected(@NonNull Uri uri, String contentType); void onMediaSelected(@NonNull Uri uri, String contentType);
} }

View File

@ -1,272 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.content.Context;
import android.graphics.PorterDuff;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnticipateOvershootInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.session.libsession.utilities.ViewUtil;
import network.loki.messenger.R;
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
enum State {
NOT_RUNNING,
RUNNING_HELD,
RUNNING_LOCKED
}
public static final int ANIMATION_DURATION = 200;
private FloatingRecordButton floatingRecordButton;
private LockDropTarget lockDropTarget;
private @Nullable Listener listener;
private @NonNull State state = State.NOT_RUNNING;
public MicrophoneRecorderView(Context context) {
super(context);
}
public MicrophoneRecorderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab));
lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target));
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
recordButton.setOnTouchListener(this);
}
public void cancelAction() {
if (state != State.NOT_RUNNING) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordCanceled();
}
}
public boolean isRecordingLocked() {
return state == State.RUNNING_LOCKED;
}
private void lockAction() {
if (state == State.RUNNING_HELD) {
state = State.RUNNING_LOCKED;
hideUi();
if (listener != null) listener.onRecordLocked();
}
}
public void unlockAction() {
if (state == State.RUNNING_LOCKED) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordReleased();
}
}
private void hideUi() {
floatingRecordButton.hide();
lockDropTarget.hide();
}
@Override
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
if (listener != null) listener.onRecordPermissionRequired();
} else {
state = State.RUNNING_HELD;
floatingRecordButton.display(event.getX(), event.getY());
lockDropTarget.display();
if (listener != null) listener.onRecordPressed();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (this.state == State.RUNNING_HELD) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordReleased();
}
break;
case MotionEvent.ACTION_MOVE:
if (this.state == State.RUNNING_HELD) {
this.floatingRecordButton.moveTo(event.getX(), event.getY());
if (listener != null) listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX());
int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
if (floatingRecordButton.lastOffsetY <= dimensionPixelSize) {
lockAction();
}
}
break;
}
return false;
}
public void setListener(@Nullable Listener listener) {
this.listener = listener;
}
public interface Listener {
void onRecordPressed();
void onRecordReleased();
void onRecordCanceled();
void onRecordLocked();
void onRecordMoved(float offsetX, float absoluteX);
void onRecordPermissionRequired();
}
private static class FloatingRecordButton {
private final ImageView recordButtonFab;
private float startPositionX;
private float startPositionY;
private float lastOffsetX;
private float lastOffsetY;
FloatingRecordButton(Context context, ImageView recordButtonFab) {
this.recordButtonFab = recordButtonFab;
this.recordButtonFab.getBackground().setColorFilter(context.getResources()
.getColor(R.color.destructive),
PorterDuff.Mode.SRC_IN);
}
void display(float x, float y) {
this.startPositionX = x;
this.startPositionY = y;
recordButtonFab.setVisibility(View.VISIBLE);
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0));
animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f,
Animation.RELATIVE_TO_SELF, .5f,
Animation.RELATIVE_TO_SELF, .5f));
animation.setDuration(ANIMATION_DURATION);
animation.setInterpolator(new OvershootInterpolator());
recordButtonFab.startAnimation(animation);
}
void moveTo(float x, float y) {
lastOffsetX = getXOffset(x);
lastOffsetY = getYOffset(y);
if (Math.abs(lastOffsetX) > Math.abs(lastOffsetY)) {
lastOffsetY = 0;
} else {
lastOffsetX = 0;
}
recordButtonFab.setTranslationX(lastOffsetX);
recordButtonFab.setTranslationY(lastOffsetY);
}
void hide() {
recordButtonFab.setTranslationX(0);
recordButtonFab.setTranslationY(0);
if (recordButtonFab.getVisibility() != VISIBLE) return;
AnimationSet animation = new AnimationSet(false);
Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, lastOffsetX,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, lastOffsetY,
Animation.ABSOLUTE, 0);
scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
translateAnimation.setInterpolator(new DecelerateInterpolator());
animation.addAnimation(scaleAnimation);
animation.addAnimation(translateAnimation);
animation.setDuration(ANIMATION_DURATION);
animation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
recordButtonFab.setVisibility(View.GONE);
recordButtonFab.clearAnimation();
recordButtonFab.startAnimation(animation);
}
private float getXOffset(float x) {
return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ?
-Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX);
}
private float getYOffset(float y) {
return Math.min(0, y - this.startPositionY);
}
}
private static class LockDropTarget {
private final View lockDropTarget;
private final int dropTargetPosition;
LockDropTarget(Context context, View lockDropTarget) {
this.lockDropTarget = lockDropTarget;
this.dropTargetPosition = context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
}
void display() {
lockDropTarget.setScaleX(1);
lockDropTarget.setScaleY(1);
lockDropTarget.setAlpha(0);
lockDropTarget.setTranslationY(0);
lockDropTarget.setVisibility(VISIBLE);
lockDropTarget.animate()
.setStartDelay(ANIMATION_DURATION * 2)
.setDuration(ANIMATION_DURATION)
.setInterpolator(new DecelerateInterpolator())
.translationY(dropTargetPosition)
.alpha(1)
.start();
}
void hide() {
lockDropTarget.animate()
.setStartDelay(0)
.setDuration(ANIMATION_DURATION)
.setInterpolator(new LinearInterpolator())
.scaleX(0).scaleY(0)
.start();
}
}
}

View File

@ -1,532 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.utilities.Conversions;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.Util;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import network.loki.messenger.R;
/**
* A cursor adapter for a conversation thread. Ultimately
* used by ComposeMessageActivity to display a conversation
* thread in a ListActivity.
*
* @author Moxie Marlinspike
*
*/
public class ConversationAdapter <V extends View & BindableConversationItem>
extends FastCursorRecyclerViewAdapter<ConversationAdapter.ViewHolder, MessageRecord>
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
{
private static final int MAX_CACHE_SIZE = 1000;
private static final String TAG = ConversationAdapter.class.getSimpleName();
private final Map<String,SoftReference<MessageRecord>> messageRecordCache =
Collections.synchronizedMap(new LRUCache<String, SoftReference<MessageRecord>>(MAX_CACHE_SIZE));
private final SparseArray<String> positionToCacheRef = new SparseArray<>();
private static final int MESSAGE_TYPE_OUTGOING = 0;
private static final int MESSAGE_TYPE_INCOMING = 1;
private static final int MESSAGE_TYPE_UPDATE = 2;
private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3;
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5;
private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
private static final int MESSAGE_TYPE_INVITATION_OUTGOING = 9;
private static final int MESSAGE_TYPE_INVITATION_INCOMING = 10;
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
private final @Nullable ItemClickListener clickListener;
private final @NonNull
GlideRequests glideRequests;
private final @NonNull Locale locale;
private final @NonNull Recipient recipient;
private final @NonNull MmsSmsDatabase db;
private final @NonNull LayoutInflater inflater;
private final @NonNull Calendar calendar;
private final @NonNull MessageDigest digest;
private MessageRecord recordToPulseHighlight;
private String searchQuery;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
super(itemView);
}
@SuppressWarnings("unchecked")
public <V extends View & BindableConversationItem> V getView() {
return (V)itemView;
}
}
static class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView textView;
HeaderViewHolder(View itemView) {
super(itemView);
textView = ViewUtil.findById(itemView, R.id.text);
}
HeaderViewHolder(TextView textView) {
super(textView);
this.textView = textView;
}
public void setText(CharSequence text) {
textView.setText(text);
}
}
interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MessageRecord item);
void onItemLongClick(MessageRecord item);
}
@SuppressWarnings("ConstantConditions")
@VisibleForTesting
ConversationAdapter(Context context, Cursor cursor) {
super(context, cursor);
try {
this.glideRequests = null;
this.locale = null;
this.clickListener = null;
this.recipient = null;
this.inflater = null;
this.db = null;
this.calendar = null;
this.digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError("SHA1 isn't supported!");
}
}
public ConversationAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@Nullable Cursor cursor,
@NonNull Recipient recipient)
{
super(context, cursor);
try {
this.glideRequests = glideRequests;
this.locale = locale;
this.clickListener = clickListener;
this.recipient = recipient;
this.inflater = LayoutInflater.from(context);
this.db = DatabaseFactory.getMmsSmsDatabase(context);
this.calendar = Calendar.getInstance();
this.digest = MessageDigest.getInstance("SHA1");
setHasStableIds(true);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError("SHA1 isn't supported!");
}
}
@Override
public void changeCursor(Cursor cursor) {
messageRecordCache.clear();
positionToCacheRef.clear();
super.cleanFastRecords();
super.changeCursor(cursor);
}
@Override
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) {
int adapterPosition = viewHolder.getAdapterPosition();
String prevCachedId = positionToCacheRef.get(adapterPosition + 1,null);
String nextCachedId = positionToCacheRef.get(adapterPosition - 1, null);
MessageRecord previousRecord = null;
if (adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1)) {
if (prevCachedId != null && messageRecordCache.containsKey(prevCachedId)) {
SoftReference<MessageRecord> prevSoftRecord = messageRecordCache.get(prevCachedId);
MessageRecord prevCachedRecord = prevSoftRecord.get();
if (prevCachedRecord != null) {
previousRecord = prevCachedRecord;
} else {
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
}
} else {
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
}
}
MessageRecord nextRecord = null;
if (adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1)) {
if (nextCachedId != null && messageRecordCache.containsKey(nextCachedId)) {
SoftReference<MessageRecord> nextSoftRecord = messageRecordCache.get(nextCachedId);
MessageRecord nextCachedRecord = nextSoftRecord.get();
if (nextCachedRecord != null) {
nextRecord = nextCachedRecord;
} else {
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
}
} else {
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
}
}
viewHolder.getView().bind(messageRecord,
Optional.fromNullable(previousRecord),
Optional.fromNullable(nextRecord),
glideRequests,
locale,
batchSelected,
recipient,
searchQuery,
messageRecord == recordToPulseHighlight);
if (messageRecord == recordToPulseHighlight) {
recordToPulseHighlight = null;
}
}
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
long start = System.currentTimeMillis();
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
itemView.setOnClickListener(view -> {
if (clickListener != null) {
clickListener.onItemClick(itemView.getMessageRecord());
}
});
itemView.setOnLongClickListener(view -> {
if (clickListener != null) {
clickListener.onItemLongClick(itemView.getMessageRecord());
}
return true;
});
itemView.setEventListener(clickListener);
Log.d(TAG, "Inflate time: " + (System.currentTimeMillis() - start));
return new ViewHolder(itemView);
}
@Override
public void onItemViewRecycled(ViewHolder holder) {
holder.getView().unbind();
}
private @LayoutRes int getLayoutForViewType(int viewType) {
switch (viewType) {
case MESSAGE_TYPE_AUDIO_OUTGOING:
case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
case MESSAGE_TYPE_DOCUMENT_OUTGOING:
case MESSAGE_TYPE_INVITATION_OUTGOING:
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
case MESSAGE_TYPE_AUDIO_INCOMING:
case MESSAGE_TYPE_THUMBNAIL_INCOMING:
case MESSAGE_TYPE_DOCUMENT_INCOMING:
case MESSAGE_TYPE_INVITATION_INCOMING:
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
}
}
@Override
public int getItemViewType(@NonNull MessageRecord messageRecord) {
if (messageRecord.isUpdate()) {
return MESSAGE_TYPE_UPDATE;
} else if (messageRecord.isOpenGroupInvitation()) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_INVITATION_OUTGOING;
else return MESSAGE_TYPE_INVITATION_INCOMING;
} else if (hasAudio(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
else return MESSAGE_TYPE_AUDIO_INCOMING;
} else if (hasDocument(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_DOCUMENT_OUTGOING;
else return MESSAGE_TYPE_DOCUMENT_INCOMING;
} else if (hasThumbnail(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING;
else return MESSAGE_TYPE_THUMBNAIL_INCOMING;
} else if (messageRecord.isOutgoing()) {
return MESSAGE_TYPE_OUTGOING;
} else {
return MESSAGE_TYPE_INCOMING;
}
}
@Override
protected boolean isRecordForId(@NonNull MessageRecord record, long id) {
return record.getId() == id;
}
@Override
public long getItemId(@NonNull Cursor cursor) {
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(cursor);
List<DatabaseAttachment> messageAttachments = Stream.of(attachments).filterNot(DatabaseAttachment::isQuote).toList();
if (messageAttachments.size() > 0 && messageAttachments.get(0).getFastPreflightId() != null) {
return Long.valueOf(messageAttachments.get(0).getFastPreflightId());
}
final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));
final byte[] bytes = digest.digest(unique.getBytes());
return Conversions.byteArrayToLong(bytes);
}
@Override
protected long getItemId(@NonNull MessageRecord record) {
if (record.isOutgoing() && record.isMms()) {
MmsMessageRecord mmsRecord = (MmsMessageRecord) record;
SlideDeck slideDeck = mmsRecord.getSlideDeck();
if (slideDeck.getThumbnailSlide() != null && slideDeck.getThumbnailSlide().getFastPreflightId() != null) {
return Long.valueOf(slideDeck.getThumbnailSlide().getFastPreflightId());
}
}
return record.getId();
}
@Override
protected MessageRecord getRecordFromCursor(@NonNull Cursor cursor) {
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
final SoftReference<MessageRecord> reference = messageRecordCache.get(type + messageId);
if (reference != null) {
final MessageRecord record = reference.get();
if (record != null) return record;
}
final MessageRecord messageRecord = db.readerFor(cursor).getCurrent();
messageRecordCache.put(type + messageId, new SoftReference<>(messageRecord));
return messageRecord;
}
public void close() {
getCursor().close();
}
public int findLastSeenPosition(long lastSeen) {
if (lastSeen <= 0) return -1;
if (!isActiveCursor()) return -1;
int count = getItemCount() - (hasFooterView() ? 1 : 0);
for (int i=(hasHeaderView() ? 1 : 0);i<count;i++) {
MessageRecord messageRecord = getRecordForPositionOrThrow(i);
if (messageRecord.isOutgoing() || messageRecord.getDateReceived() <= lastSeen) {
return i;
}
}
return -1;
}
public void toggleSelection(MessageRecord messageRecord) {
if (!batchSelected.remove(messageRecord)) {
batchSelected.add(messageRecord);
}
}
public void clearSelection() {
batchSelected.clear();
}
public Set<MessageRecord> getSelectedItems() {
return Collections.unmodifiableSet(new HashSet<>(batchSelected));
}
public void pulseHighlightItem(int position) {
if (position < getItemCount()) {
recordToPulseHighlight = getRecordForPositionOrThrow(position);
notifyItemChanged(position);
}
}
public void onSearchQueryUpdated(@Nullable String query) {
this.searchQuery = query;
notifyDataSetChanged();
}
private boolean hasAudio(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
}
private boolean hasDocument(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
private boolean hasThumbnail(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
}
@Override
public long getHeaderId(int position) {
if (!isActiveCursor()) return -1;
if (isHeaderPosition(position)) return -1;
if (isFooterPosition(position)) return -1;
if (position >= getItemCount()) return -1;
if (position < 0) return -1;
MessageRecord record = getRecordForPositionOrThrow(position);
if (record.getRecipient().getAddress().isOpenGroup()) {
calendar.setTime(new Date(record.getDateReceived()));
} else {
calendar.setTime(new Date(record.getDateSent()));
}
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
}
public long getReceivedTimestamp(int position) {
if (!isActiveCursor()) return 0;
if (isHeaderPosition(position)) return 0;
if (isFooterPosition(position)) return 0;
if (position >= getItemCount()) return 0;
if (position < 0) return 0;
MessageRecord messageRecord = getRecordForPositionOrThrow(position);
if (messageRecord.isOutgoing()) return 0;
else return messageRecord.getDateReceived();
}
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
}
public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
}
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
MessageRecord messageRecord = getRecordForPositionOrThrow(position);
long timestamp = messageRecord.getDateReceived();
if (recipient.getAddress().isOpenGroup()) { timestamp = messageRecord.getTimestamp(); }
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, timestamp));
}
public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) {
viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
}
static class LastSeenHeader extends StickyHeaderDecoration {
private final ConversationAdapter adapter;
private final long lastSeenTimestamp;
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
super(adapter, false, false);
this.adapter = adapter;
this.lastSeenTimestamp = lastSeenTimestamp;
}
@Override
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
if (!adapter.isActiveCursor()) {
return false;
}
if (lastSeenTimestamp <= 0) {
return false;
}
long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
}
@Override
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
return parent.getLayoutManager().getDecoratedTop(child);
}
@Override
protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent);
adapter.onBindLastSeenViewHolder(viewHolder, position);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
viewHolder.itemView.measure(childWidth, childHeight);
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
return viewHolder;
}
}
}

View File

@ -1,120 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.core.app.ActivityOptionsCompat;
import android.view.Display;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.WindowManager;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ListenableFuture;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.concurrent.ExecutionException;
import network.loki.messenger.R;
public class ConversationPopupActivity extends ConversationActivity {
private static final String TAG = ConversationPopupActivity.class.getSimpleName();
@Override
protected void onPreCreate() {
super.onPreCreate();
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
protected void onCreate(Bundle bundle, boolean ready) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 1.0f;
params.dimAmount = 0.1f;
params.gravity = Gravity.TOP;
getWindow().setAttributes(params);
Display display = getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
super.onCreate(bundle, ready);
}
@Override
protected void onResume() {
super.onResume();
composeText.requestFocus();
quickAttachmentToggle.disable();
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.conversation_popup, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_expand:
saveDraft().addListener(new ListenableFuture.Listener<Long>() {
@Override
public void onSuccess(Long result) {
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, getRecipient().getAddress());
intent.putExtra(ConversationActivityV2.THREAD_ID, result);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
startActivity(intent, transition.toBundle());
} else {
startActivity(intent);
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
}
finish();
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
}
});
return true;
}
return false;
}
@Override
protected void initializeActionBar() {
super.initializeActionBar();
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
@Override
protected void sendComplete(long threadId) {
super.sendComplete(threadId);
finish();
}
}

View File

@ -1,146 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.session.libsession.utilities.Debouncer;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.io.Closeable;
import java.util.List;
public class ConversationSearchViewModel extends AndroidViewModel {
private final SearchRepository searchRepository;
private final CloseableLiveData<SearchResult> result;
private final Debouncer debouncer;
private boolean firstSearch;
private boolean searchOpen;
private String activeQuery;
private long activeThreadId;
public ConversationSearchViewModel(@NonNull Application application) {
super(application);
Context context = application.getApplicationContext();
result = new CloseableLiveData<>();
debouncer = new Debouncer(500);
searchRepository = new SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getThreadDatabase(context),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL);
}
LiveData<SearchResult> getSearchResults() {
return result;
}
void onQueryUpdated(@NonNull String query, long threadId) {
if (firstSearch && query.length() < 2) {
result.postValue(new SearchResult(CursorList.emptyList(), 0));
return;
}
if (query.equals(activeQuery)) {
return;
}
updateQuery(query, threadId);
}
void onMissingResult() {
if (activeQuery != null) {
updateQuery(activeQuery, activeThreadId);
}
}
void onMoveUp() {
debouncer.clear();
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1);
result.setValue(new SearchResult(messages, position), false);
}
void onMoveDown() {
debouncer.clear();
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.max(result.getValue().getPosition() - 1, 0);
result.setValue(new SearchResult(messages, position), false);
}
void onSearchOpened() {
searchOpen = true;
firstSearch = true;
}
void onSearchClosed() {
searchOpen = false;
debouncer.clear();
result.close();
}
@Override
protected void onCleared() {
super.onCleared();
result.close();
}
private void updateQuery(@NonNull String query, long threadId) {
activeQuery = query;
activeThreadId = threadId;
debouncer.publish(() -> {
firstSearch = false;
searchRepository.query(query, threadId, messages -> {
Util.runOnMain(() -> {
if (searchOpen && query.equals(activeQuery)) {
result.setValue(new SearchResult(messages, 0));
} else {
messages.close();
}
});
});
});
}
static class SearchResult implements Closeable {
private final CursorList<MessageResult> results;
private final int position;
SearchResult(CursorList<MessageResult> results, int position) {
this.results = results;
this.position = position;
}
public List<MessageResult> getResults() {
return results;
}
public int getPosition() {
return position;
}
@Override
public void close() {
results.close();
}
}
}

View File

@ -1,198 +0,0 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.DateUtils;
import java.util.Locale;
import java.util.Set;
import network.loki.messenger.R;
//TODO Remove this class.
public class ConversationUpdateItem extends LinearLayout
implements RecipientModifiedListener, BindableConversationItem
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
private Set<MessageRecord> batchSelected;
private ImageView icon;
private TextView title;
private TextView body;
private TextView date;
private Recipient sender;
private MessageRecord messageRecord;
private Locale locale;
public ConversationUpdateItem(Context context) {
super(context);
}
public ConversationUpdateItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.icon = findViewById(R.id.conversation_update_icon);
this.title = findViewById(R.id.conversation_update_title);
this.body = findViewById(R.id.conversation_update_body);
this.date = findViewById(R.id.conversation_update_date);
this.setOnClickListener(new InternalClickListener(null));
}
@Override
public void bind(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseUpdate)
{
this.batchSelected = batchSelected;
bind(messageRecord, locale);
}
@Override
public void setEventListener(@Nullable EventListener listener) {
// No events to report yet
}
@Override
public MessageRecord getMessageRecord() {
return messageRecord;
}
private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
this.messageRecord = messageRecord;
this.sender = messageRecord.getIndividualRecipient();
this.locale = locale;
this.sender.addListener(this);
if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isScreenshotNotification()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT);
else if (messageRecord.isMediaSavedNotification()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED);
else throw new AssertionError("Neither group nor log nor joined.");
if (batchSelected.contains(messageRecord)) setSelected(true);
else setSelected(false);
}
private void setCallRecord(MessageRecord messageRecord) {
if (messageRecord.isIncomingCall()) icon.setImageResource(R.drawable.ic_call_received_grey600_24dp);
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
body.setText(messageRecord.getDisplayBody(getContext()));
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
title.setVisibility(GONE);
body.setVisibility(VISIBLE);
date.setVisibility(View.VISIBLE);
}
private void setTimerRecord(final MessageRecord messageRecord) {
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
if (messageRecord.getExpiresIn() > 0) {
icon.setImageResource(R.drawable.ic_timer);
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
} else {
icon.setImageResource(R.drawable.ic_timer_disabled);
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
}
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(VISIBLE);
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) {
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) {
icon.setImageResource(R.drawable.quick_camera_dark);
} else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) {
icon.setImageResource(R.drawable.ic_file_download_white_36dp);
}
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(VISIBLE);
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
private void setTextMessageRecord(MessageRecord messageRecord) {
body.setText(messageRecord.getDisplayBody(getContext()));
icon.setVisibility(GONE);
title.setVisibility(GONE);
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
@Override
public void onModified(Recipient recipient) {
Util.runOnMain(() -> bind(messageRecord, locale));
}
@Override
public void setOnClickListener(View.OnClickListener l) {
super.setOnClickListener(new InternalClickListener(l));
}
@Override
public void unbind() {
if (sender != null) {
sender.removeListener(this);
}
}
private class InternalClickListener implements View.OnClickListener {
@Nullable private final View.OnClickListener parent;
InternalClickListener(@Nullable View.OnClickListener parent) {
this.parent = parent;
}
@Override
public void onClick(View v) {
}
}
}

View File

@ -78,7 +78,7 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.audio.AudioRecorder import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.dialogs.* import org.thoughtcrime.securesms.conversation.v2.dialogs.*
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
@ -902,7 +902,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
message.text = body message.text = body
val quote = quotedMessage?.let { val quote = quotedMessage?.let {
val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf()
QuoteModel(it.dateSent, it.individualRecipient.address, it.body, false, quotedAttachments) val sender = if (it.isOutgoing) fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) else it.individualRecipient.address
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
} }
val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview) val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview)
// Clear the input bar // Clear the input bar
@ -1048,10 +1049,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val future = audioRecorder.stopRecording() val future = audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
future.addListener(object : ListenableFuture.Listener<Pair<Uri?, Long?>> { future.addListener(object : ListenableFuture.Listener<Pair<Uri, Long>> {
override fun onSuccess(result: Pair<Uri?, Long?>) { override fun onSuccess(result: Pair<Uri, Long>) {
val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second!!, MediaTypes.AUDIO_AAC, true) val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second, MediaTypes.AUDIO_AAC, true)
val slideDeck = SlideDeck() val slideDeck = SlideDeck()
slideDeck.addSlide(audioSlide) slideDeck.addSlide(audioSlide)
sendAttachments(slideDeck.asAttachments(), null) sendAttachments(slideDeck.asAttachments(), null)

View File

@ -38,7 +38,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
set(value) { field = value; showOrHideInputIfNeeded() } set(value) { field = value; showOrHideInputIfNeeded() }
var text: String var text: String
get() { return inputBarEditText.text.toString() } get() { return inputBarEditText.text?.toString() ?: "" }
set(value) { inputBarEditText.setText(value) } set(value) { inputBarEditText.setText(value) }
private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) } private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) }
@ -122,7 +122,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt() val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt()
val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize() val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize()
quoteView.bind(sender, message.body, attachments, quoteView.bind(sender, message.body, attachments,
thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, false, glide)
// The 6 DP below is the padding the quote view applies to itself, which isn't included in the // The 6 DP below is the padding the quote view applies to itself, which isn't included in the
// intrinsic height calculation. // intrinsic height calculation.
val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources)

View File

@ -110,7 +110,8 @@ class QuoteView : LinearLayout {
// region Updating // region Updating
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, glide: GlideRequests) { isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long,
isOriginalMissing: Boolean, glide: GlideRequests) {
val contactDB = DatabaseFactory.getSessionContactDatabase(context) val contactDB = DatabaseFactory.getSessionContactDatabase(context)
// Reduce the max body text view line count to 2 if this is a group thread because // Reduce the max body text view line count to 2 if this is a group thread because
// we'll be showing the author text view and we don't want the overall quote view height // we'll be showing the author text view and we don't want the overall quote view height
@ -128,7 +129,7 @@ class QuoteView : LinearLayout {
quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context);
quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage))
// Accent line / attachment preview // Accent line / attachment preview
val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing
quoteViewAccentLine.isVisible = !hasAttachments quoteViewAccentLine.isVisible = !hasAttachments
quoteViewAttachmentPreviewContainer.isVisible = hasAttachments quoteViewAttachmentPreviewContainer.isVisible = hasAttachments
if (!hasAttachments) { if (!hasAttachments) {
@ -136,8 +137,7 @@ class QuoteView : LinearLayout {
accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height
quoteViewAccentLine.layoutParams = accentLineLayoutParams quoteViewAccentLine.layoutParams = accentLineLayoutParams
quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage))
} else { } else if (attachments != null) {
attachments!!
quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme))
val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent
val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme)

View File

@ -82,12 +82,18 @@ class VisibleMessageContentView : LinearLayout {
} else if (message is MmsMessageRecord && message.quote != null) { } else if (message is MmsMessageRecord && message.quote != null) {
val quote = message.quote!! val quote = message.quote!!
val quoteView = QuoteView(context, QuoteView.Mode.Regular) val quoteView = QuoteView(context, QuoteView.Mode.Regular)
// The max content width is the max message bubble size - 2 times the horizontal padding - the // The max content width is the max message bubble size - 2 times the horizontal padding - 2
// quote view content area's start margin. This unfortunately has to be calculated manually // times the horizontal margin. This unfortunately has to be calculated manually
// here to get the layout right. // here to get the layout right.
val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt() val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - 2 * toPx(16, resources)).roundToInt()
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, val quoteText = if (quote.isOriginalMissing) {
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) context.getString(R.string.QuoteView_original_missing)
} else {
quote.text
}
quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId,
quote.isOriginalMissing, glide)
mainContainer.addView(quoteView) mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)

View File

@ -4,15 +4,18 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_voice_message.view.* import kotlinx.android.synthetic.main.view_voice_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -21,6 +24,10 @@ import kotlin.math.roundToLong
class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var isPlaying = false private var isPlaying = false
set(value) {
field = value
renderIcon()
}
private var progress = 0.0 private var progress = 0.0
private var duration = 0L private var duration = 0L
private var player: AudioSlidePlayer? = null private var player: AudioSlidePlayer? = null
@ -44,29 +51,36 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
// region Updating // region Updating
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
val audio = message.slideDeck.audioSlide!! val audio = message.slideDeck.audioSlide!!
val player = AudioSlidePlayer.createFor(context, audio, this)
this.player = player
isPreparing = true
if (!audio.isPendingDownload && !audio.isInProgress) {
player.play(0.0)
}
voiceMessageViewLoader.isVisible = audio.isPendingDownload voiceMessageViewLoader.isVisible = audio.isPendingDownload
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])
cornerMask.setTopRightRadius(cornerRadii[1]) cornerMask.setTopRightRadius(cornerRadii[1])
cornerMask.setBottomRightRadius(cornerRadii[2]) cornerMask.setBottomRightRadius(cornerRadii[2])
cornerMask.setBottomLeftRadius(cornerRadii[3]) cornerMask.setBottomLeftRadius(cornerRadii[3])
// only process audio if downloaded
if (audio.isPendingDownload || audio.isInProgress) {
this.player = null
return
} }
override fun onPlayerStart(player: AudioSlidePlayer) { val player = AudioSlidePlayer.createFor(context, audio, this)
if (!isPreparing) { return } this.player = player
isPreparing = false
duration = player.duration (audio.asAttachment() as? DatabaseAttachment)?.let { attachment ->
DatabaseFactory.getAttachmentDatabase(context).getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
if (audioExtras.durationMs > 0) {
duration = audioExtras.durationMs
voiceMessageViewDurationTextView.visibility = View.VISIBLE
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(duration), TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
TimeUnit.MILLISECONDS.toSeconds(duration)) TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
player.stop()
} }
}
}
}
override fun onPlayerStart(player: AudioSlidePlayer) {}
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) { override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) {
if (progress == 1.0) { if (progress == 1.0) {
@ -88,20 +102,27 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
progressView.layoutParams = layoutParams progressView.layoutParams = layoutParams
} }
override fun onPlayerStop(player: AudioSlidePlayer) { } override fun onPlayerStop(player: AudioSlidePlayer) {
Log.d("Loki", "Player stopped")
isPlaying = false
}
override fun dispatchDraw(canvas: Canvas) { override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
} }
private fun renderIcon() {
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
voiceMessagePlaybackImageView.setImageResource(iconID)
}
// endregion // endregion
// region Interaction // region Interaction
fun togglePlayback() { fun togglePlayback() {
val player = this.player ?: return val player = this.player ?: return
isPlaying = !isPlaying isPlaying = !isPlaying
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
voiceMessagePlaybackImageView.setImageResource(iconID)
if (isPlaying) { if (isPlaying) {
player.play(progress) player.play(progress)
} else { } else {

View File

@ -44,8 +44,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras;
import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.ExternalStorageUtil; import org.session.libsignal.utilities.ExternalStorageUtil;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
@ -820,7 +820,7 @@ public class AttachmentDatabase extends Database {
* @return true if the update operation was successful. * @return true if the update operation was successful.
*/ */
@Synchronized @Synchronized
public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) { public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras, long threadId) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples()); values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples());
values.put(AUDIO_DURATION, extras.getDurationMs()); values.put(AUDIO_DURATION, extras.getDurationMs());
@ -830,9 +830,22 @@ public class AttachmentDatabase extends Database {
PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE, PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE,
extras.getAttachmentId().toStrings()); extras.getAttachmentId().toStrings());
if (threadId >= 0) {
notifyConversationListeners(threadId);
}
return alteredRows > 0; return alteredRows > 0;
} }
/**
* Updates audio extra columns for the "audio/*" mime type attachments only.
* @return true if the update operation was successful.
*/
@Synchronized
public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) {
return setAttachmentAudioExtras(extras, -1); // -1 for no update
}
@VisibleForTesting @VisibleForTesting
class ThumbnailFetchCallable implements Callable<InputStream> { class ThumbnailFetchCallable implements Callable<InputStream> {

View File

@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
@ -881,6 +882,20 @@ public class MmsDatabase extends MessagingDatabase {
} }
} }
public void deleteQuotedFromMessages(MessageRecord toDeleteRecord) {
String query = THREAD_ID + " = ?";
Cursor threadMmsCursor = rawQuery(query, new String[]{String.valueOf(toDeleteRecord.getThreadId())});
Reader reader = readerFor(threadMmsCursor);
MmsMessageRecord messageRecord;
while ((messageRecord = (MmsMessageRecord) reader.getNext()) != null) {
if (messageRecord.getQuote() != null && toDeleteRecord.getDateSent() == messageRecord.getQuote().getId()) {
setQuoteMissing(messageRecord.getId());
}
}
reader.close();
}
public boolean delete(long messageId) { public boolean delete(long messageId) {
long threadId = getThreadIdForMessage(messageId); long threadId = getThreadIdForMessage(messageId);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
@ -889,6 +904,12 @@ public class MmsDatabase extends MessagingDatabase {
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId); groupReceiptDatabase.deleteRowsForMessage(messageId);
MessageRecord toDelete;
try (Cursor messageCursor = getMessage(messageId)) {
toDelete = readerFor(messageCursor).getNext();
}
deleteQuotedFromMessages(toDelete);
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
@ -1066,6 +1087,14 @@ public class MmsDatabase extends MessagingDatabase {
return new OutgoingMessageReader(message, threadId); return new OutgoingMessageReader(message, threadId);
} }
public int setQuoteMissing(long messageId) {
ContentValues contentValues = new ContentValues();
contentValues.put(QUOTE_MISSING, 1);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
int rows = database.update(TABLE_NAME, contentValues, ID + " = ?", new String[]{ String.valueOf(messageId) });
return rows;
}
public static class Status { public static class Status {
public static final int DOWNLOAD_INITIALIZED = 1; public static final int DOWNLOAD_INITIALIZED = 1;
public static final int DOWNLOAD_NO_CONNECTIVITY = 2; public static final int DOWNLOAD_NO_CONNECTIVITY = 2;

View File

@ -514,6 +514,12 @@ public class SmsDatabase extends MessagingDatabase {
Log.i("MessageDatabase", "Deleting: " + messageId); Log.i("MessageDatabase", "Deleting: " + messageId);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId); long threadId = getThreadIdForMessage(messageId);
try {
SmsMessageRecord toDelete = getMessage(messageId);
DatabaseFactory.getMmsDatabase(context).deleteQuotedFromMessages(toDelete);
} catch (NoSuchMessageException e) {
Log.e(TAG, "Couldn't find message record for messageId "+messageId, e);
}
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId); notifyConversationListeners(threadId);

View File

@ -27,7 +27,6 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
@ -190,7 +189,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return
JobQueue.shared.add(job) JobQueue.shared.resumePendingSendMessage(job)
} }
override fun isJobCanceled(job: Job): Boolean { override fun isJobCanceled(job: Job): Boolean {

View File

@ -17,7 +17,7 @@ import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes import org.session.libsession.utilities.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -138,7 +138,6 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivityV2::class.java) val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent) context.startActivity(intent)
} }

View File

@ -29,7 +29,7 @@ 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.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
@ -114,11 +114,9 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
val intent = Intent(this, ConversationActivityV2::class.java) val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
intent.setDataAndType(getIntent().data, getIntent().type) intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread) intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent) startActivity(intent)
finish() finish()
} }

View File

@ -37,7 +37,7 @@ import org.session.libsignal.utilities.toHexString
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord

View File

@ -32,7 +32,7 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
@ -130,7 +130,6 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivityV2::class.java) val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent) context.startActivity(intent)
} }

View File

@ -14,7 +14,6 @@ import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_link_device.* import kotlinx.android.synthetic.main.activity_link_device.*
import kotlinx.android.synthetic.main.conversation_activity.*
import kotlinx.android.synthetic.main.fragment_recovery_phrase.* import kotlinx.android.synthetic.main.fragment_recovery_phrase.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@ -14,7 +14,7 @@ import kotlinx.android.synthetic.main.activity_qr_code.*
import kotlinx.android.synthetic.main.fragment_view_my_qr_code.* import kotlinx.android.synthetic.main.fragment_view_my_qr_code.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes import org.session.libsession.utilities.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -56,11 +56,9 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false) val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
val intent = Intent(this, ConversationActivityV2::class.java) val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
intent.setDataAndType(getIntent().data, getIntent().type) intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread) intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent) startActivity(intent)
finish() finish()
} }

View File

@ -27,14 +27,12 @@ import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ProfileKeyUtil
import org.thoughtcrime.securesms.ApplicationContext
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.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
@ -260,7 +258,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
intent.action = Intent.ACTION_SEND intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey) intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
intent.type = "text/plain" intent.type = "text/plain"
startActivity(intent) val chooser = Intent.createChooser(intent, getString(R.string.share))
startActivity(chooser)
} }
private fun showPrivacySettings() { private fun showPrivacySettings() {
@ -284,7 +283,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey!" val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey!"
intent.putExtra(Intent.EXTRA_TEXT, invitation) intent.putExtra(Intent.EXTRA_TEXT, invitation)
intent.type = "text/plain" intent.type = "text/plain"
startActivity(intent) val chooser = Intent.createChooser(intent, getString(R.string.activity_settings_invite_button_title))
startActivity(chooser)
} }
private fun helpTranslate() { private fun helpTranslate() {

View File

@ -9,10 +9,11 @@ import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.BaseJob import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.loki.utilities.DecodedAudio
import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.PartAuthority
import java.io.InputStream import java.io.InputStream
import java.lang.IllegalStateException import java.lang.IllegalStateException
@ -133,35 +134,4 @@ class PrepareAttachmentAudioExtrasJob : BaseJob {
/** Gets dispatched once the audio extras have been updated. */ /** Gets dispatched once the audio extras have been updated. */
data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId) data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId)
@RequiresApi(Build.VERSION_CODES.M)
private class InputStreamMediaDataSource: MediaDataSource {
private val data: ByteArray
constructor(inputStream: InputStream): super() {
this.data = inputStream.readBytes()
}
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
val length: Int = data.size
if (position >= length) {
return -1 // -1 indicates EOF
}
var actualSize = size
if (position + size > length) {
actualSize -= (position + size - length).toInt()
}
System.arraycopy(data, position.toInt(), buffer, offset, actualSize)
return actualSize
}
override fun getSize(): Long {
return data.size.toLong()
}
override fun close() {
// We don't need to close the wrapped stream.
}
}
} }

View File

@ -14,7 +14,7 @@ import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.byteToNormalizedFloat import org.session.libsession.utilities.byteToNormalizedFloat
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min

View File

@ -48,7 +48,7 @@ import org.session.libsignal.utilities.Util;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;

View File

@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;

View File

@ -7,11 +7,10 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationPopupActivity;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.Recipient.*; import org.session.libsession.utilities.recipients.Recipient.*;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -167,9 +166,9 @@ public class NotificationState {
public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) { public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) {
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size()); if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size());
Intent intent = new Intent(context, ConversationPopupActivity.class); Intent intent = new Intent(context, ConversationActivityV2.class);
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, (long)threads.toArray()[0]); intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

View File

@ -11,7 +11,7 @@ import androidx.core.app.TaskStackBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.widget.Toast; import android.widget.Toast;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
@ -36,11 +36,6 @@ public class CommunicationActions {
Intent intent = new Intent(context, ConversationActivityV2.class); Intent intent = new Intent(context, ConversationActivityV2.class);
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress()); intent.putExtra(ConversationActivityV2.ADDRESS, recipient.getAddress());
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
if (!TextUtils.isEmpty(text)) {
intent.putExtra(ConversationActivity.TEXT_EXTRA, text);
}
if (backStack != null) { if (backStack != null) {
backStack.addNextIntent(intent); backStack.addNextIntent(intent);

View File

@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.util;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.text.TextUtils; import android.text.TextUtils;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="90"
android:dither="true"
android:endColor="@android:color/transparent"
android:startColor="@color/conversation_compose_divider" />
</shape>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="@color/touch_highlight" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item
android:id="@android:id/mask"
android:right="24dp">
<shape>
<corners
android:bottomLeftRadius="46dp"
android:topLeftRadius="46dp" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<!-- Add half of the medium_profile_picture_size padding on the right to better work with the group icons. -->
<item
android:id="@android:id/mask"
android:right="24dp">
<shape>
<corners
android:bottomLeftRadius="@dimen/medium_profile_picture_size"
android:topLeftRadius="@dimen/medium_profile_picture_size" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/accent_alpha50" android:state_selected="true" />
<item android:drawable="@color/signal_primary_alpha_focus" android:state_focused="true" />
</selector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="1000">
<item android:drawable="@color/accent_alpha50" android:state_selected="true" />
<item android:drawable="@color/signal_primary_alpha_focus" android:state_focused="true" />
</selector>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<corners android:radius="2dp" />
<solid android:color="#FFD32F2F" />
</shape>
</item>
<item>
<shape>
<corners android:radius="2dp" />
<solid android:color="#FFF44336" />
</shape>
</item>
</selector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.67,3.87L9.9,2.1 0,12l9.9,9.9 1.77,-1.77L3.54,12z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More