basic last seen feature. no peristancy just yet. no polish
This commit is contained in:
parent
a583471af8
commit
5fe926b645
|
@ -6,28 +6,29 @@
|
|||
android:background="#e5e5e5" >
|
||||
|
||||
<RelativeLayout
|
||||
android:background="#eee"
|
||||
android:id="@+id/textsend"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true">
|
||||
android:layout_alignParentLeft="true"
|
||||
android:background="#eee" >
|
||||
|
||||
<EditText
|
||||
android:id="@+id/textinput"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:layout_toLeftOf="@+id/textSendButton"
|
||||
android:background="#eee"
|
||||
android:ems="10"
|
||||
android:inputType="textShortMessage|textMultiLine|textCapSentences"
|
||||
android:minLines="1" >
|
||||
android:minHeight="48dp"
|
||||
android:minLines="1"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingTop="12dp" >
|
||||
|
||||
<requestFocus />
|
||||
</EditText>
|
||||
|
||||
|
@ -52,17 +53,16 @@
|
|||
android:divider="@null"
|
||||
android:dividerHeight="0dp"
|
||||
android:listSelector="@android:color/transparent"
|
||||
android:stackFromBottom="true"
|
||||
android:transcriptMode="alwaysScroll"
|
||||
tools:listitem="@layout/message_sent"
|
||||
android:stackFromBottom="true">
|
||||
|
||||
tools:listitem="@layout/message_sent" >
|
||||
</ListView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/info_box"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/muc_error"
|
||||
|
@ -70,83 +70,101 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/redbackground"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
>
|
||||
android:visibility="gone" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/muc_error_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#eee"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:textSize="20sp"/>
|
||||
android:textColor="#eee"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:text="Click to edit conference details"
|
||||
android:textColor="#eee"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/new_fingerprint"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/redbackground"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
>
|
||||
android:visibility="gone" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="Unknown OTR Fingerprint"
|
||||
android:textColor="#eee"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:textSize="20sp"/>
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/otr_fingerprint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/otr_fingerprint"
|
||||
android:textColor="#eee"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="14sp"
|
||||
android:typeface="monospace" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pgp_keyentry"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bluebackground"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
>
|
||||
android:visibility="gone" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="OpenPGP encrypted messages found"
|
||||
android:textColor="#eee"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:textSize="20sp"/>
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#eee"
|
||||
android:text="Click here to enter passphrase and decrypt messages"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:text="Click here to enter passphrase and decrypt messages"
|
||||
android:textColor="#eee"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/last_seen"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#7f333333"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:layout_below="@+id/info_box">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_seen_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="4dp"
|
||||
android:text="@string/last_seen"
|
||||
android:textColor="#e5e5e5"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -147,6 +147,8 @@
|
|||
<string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string>
|
||||
<string name="pref_confirm_messages">Confirm Messages</string>
|
||||
<string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string>
|
||||
<string name="pref_show_last_seen">Display last seen</string>
|
||||
<string name="pref_show_last_seen_summary">Display the latest time a contact has been seen online</string>
|
||||
<string name="openpgp_error">OpenKeychain reporeted an error</string>
|
||||
<string name="error_decrypting_file">I/O Error decrypting file</string>
|
||||
<string name="error_copying_image_file">Error copying image file.</string>
|
||||
|
@ -234,4 +236,6 @@
|
|||
<string name="hours">hours</string>
|
||||
<string name="mins">mins</string>
|
||||
<string name="missing_public_keys">Missing public key announcements</string>
|
||||
<string name="last_seen">last seen %1$s ago on %2$s</string>
|
||||
<string name="never_seen">never seen</string>
|
||||
</resources>
|
|
@ -72,6 +72,11 @@
|
|||
android:title="@string/pref_conference_name"
|
||||
android:summary="@string/pref_conference_name_summary"
|
||||
android:defaultValue="true"/>
|
||||
<CheckBoxPreference
|
||||
android:key="show_last_seen"
|
||||
android:title="@string/pref_show_last_seen"
|
||||
android:summary="@string/pref_show_last_seen_summary"
|
||||
android:defaultValue="false"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_advanced_options">
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.json.JSONObject;
|
|||
import eu.siacs.conversations.xml.Element;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
public class Contact {
|
||||
public static final String TABLENAME = "contacts";
|
||||
|
@ -39,6 +38,8 @@ public class Contact {
|
|||
|
||||
protected boolean inRoster = true;
|
||||
|
||||
public Lastseen lastseen = new Lastseen();
|
||||
|
||||
public Contact(String account, String systemName, String serverName,
|
||||
String jid, int subscription, String photoUri,
|
||||
String systemAccount, String keys) {
|
||||
|
@ -305,4 +306,9 @@ public class Contact {
|
|||
public static final int DIRTY_PUSH = 6;
|
||||
public static final int DIRTY_DELETE = 7;
|
||||
}
|
||||
|
||||
public class Lastseen {
|
||||
public long time = 0;
|
||||
public String presence = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,6 +203,15 @@ public class Message extends AbstractEntity {
|
|||
this.counterpart = this.counterpart.split("/")[0] + "/" + presence;
|
||||
}
|
||||
|
||||
public String getPresence() {
|
||||
String[] counterparts = this.counterpart.split("/");
|
||||
if (counterparts.length == 2) {
|
||||
return counterparts[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setJingleConnection(JingleConnection connection) {
|
||||
this.jingleConnection = connection;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import net.java.otr4j.session.Session;
|
|||
import net.java.otr4j.session.SessionStatus;
|
||||
import android.util.Log;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
@ -26,6 +27,7 @@ public class MessageParser {
|
|||
Conversation conversation = mXmppConnectionService
|
||||
.findOrCreateConversation(account, fromParts[0], false);
|
||||
conversation.setLatestMarkableMessageId(getMarkableMessageId(packet));
|
||||
updateLastseen(packet, account);
|
||||
String pgpBody = getPgpBody(packet);
|
||||
if (pgpBody != null) {
|
||||
return new Message(conversation, packet.getFrom(), pgpBody,
|
||||
|
@ -43,6 +45,7 @@ public class MessageParser {
|
|||
String[] fromParts = packet.getFrom().split("/");
|
||||
Conversation conversation = mXmppConnectionService
|
||||
.findOrCreateConversation(account, fromParts[0], false);
|
||||
updateLastseen(packet, account);
|
||||
String body = packet.getBody();
|
||||
if (!conversation.hasValidOtrSession()) {
|
||||
if (properlyAddressed) {
|
||||
|
@ -171,6 +174,7 @@ public class MessageParser {
|
|||
return null; // either malformed or boring
|
||||
if (status == Message.STATUS_RECIEVED) {
|
||||
fullJid = message.getAttribute("from");
|
||||
updateLastseen(message, account);
|
||||
} else {
|
||||
fullJid = message.getAttribute("to");
|
||||
}
|
||||
|
@ -211,4 +215,23 @@ public class MessageParser {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLastseen(Element message, Account account) {
|
||||
String[] fromParts = message.getAttribute("from").split("/");
|
||||
String from = fromParts[0];
|
||||
String presence = null;
|
||||
if (fromParts.length >= 2) {
|
||||
presence = fromParts[1];
|
||||
}
|
||||
Contact contact = account.getRoster().getContact(from);
|
||||
if (presence!=null) {
|
||||
contact.lastseen.presence = presence;
|
||||
contact.lastseen.time = System.currentTimeMillis();
|
||||
} else if ((contact.getPresences().size() == 1)&&(contact.getPresences().containsKey(contact.lastseen.presence))) {
|
||||
contact.lastseen.time = System.currentTimeMillis();
|
||||
} else {
|
||||
contact.lastseen.presence = null;
|
||||
contact.lastseen.time = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ public class ConversationActivity extends XmppActivity {
|
|||
|
||||
private boolean paneShouldBeOpen = true;
|
||||
private boolean useSubject = true;
|
||||
private boolean showLastseen = false;
|
||||
private ArrayAdapter<Conversation> listAdapter;
|
||||
|
||||
public Message pendingMessage = null;
|
||||
|
@ -182,15 +183,18 @@ public class ConversationActivity extends XmppActivity {
|
|||
convName.setText(conv.getName(useSubject));
|
||||
TextView convLastMsg = (TextView) view
|
||||
.findViewById(R.id.conversation_lastmsg);
|
||||
ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage);
|
||||
ImageView imagePreview = (ImageView) view
|
||||
.findViewById(R.id.conversation_lastimage);
|
||||
|
||||
Message latestMessage = conv.getLatestMessage();
|
||||
|
||||
if (latestMessage.getType() == Message.TYPE_TEXT) {
|
||||
if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)&&(latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
|
||||
if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)
|
||||
&& (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
|
||||
convLastMsg.setText(conv.getLatestMessage().getBody());
|
||||
} else {
|
||||
convLastMsg.setText(getText(R.string.encrypted_message_received));
|
||||
convLastMsg
|
||||
.setText(getText(R.string.encrypted_message_received));
|
||||
}
|
||||
convLastMsg.setVisibility(View.VISIBLE);
|
||||
imagePreview.setVisibility(View.GONE);
|
||||
|
@ -203,17 +207,17 @@ public class ConversationActivity extends XmppActivity {
|
|||
convLastMsg.setVisibility(View.VISIBLE);
|
||||
imagePreview.setVisibility(View.GONE);
|
||||
if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) {
|
||||
convLastMsg.setText(getText(R.string.image_offered_for_download));
|
||||
convLastMsg
|
||||
.setText(getText(R.string.image_offered_for_download));
|
||||
} else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) {
|
||||
convLastMsg.setText(getText(R.string.receiving_image));
|
||||
convLastMsg
|
||||
.setText(getText(R.string.receiving_image));
|
||||
} else {
|
||||
convLastMsg.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!conv.isRead()) {
|
||||
convName.setTypeface(null, Typeface.BOLD);
|
||||
convLastMsg.setTypeface(null, Typeface.BOLD);
|
||||
|
@ -223,13 +227,13 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
|
||||
((TextView) view.findViewById(R.id.conversation_lastupdate))
|
||||
.setText(UIHelper.readableTimeDifference(getContext(), conv
|
||||
.getLatestMessage().getTimeSent()));
|
||||
.setText(UIHelper.readableTimeDifference(getContext(),
|
||||
conv.getLatestMessage().getTimeSent()));
|
||||
|
||||
ImageView profilePicture = (ImageView) view
|
||||
.findViewById(R.id.conversation_image);
|
||||
profilePicture.setImageBitmap(UIHelper.getContactPicture(
|
||||
conv, 56, activity.getApplicationContext(), false));
|
||||
profilePicture.setImageBitmap(UIHelper.getContactPicture(conv,
|
||||
56, activity.getApplicationContext(), false));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -266,6 +270,11 @@ public class ConversationActivity extends XmppActivity {
|
|||
getActionBar().setTitle(R.string.app_name);
|
||||
invalidateOptionsMenu();
|
||||
hideKeyboard();
|
||||
ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
|
||||
.findFragmentByTag("conversation");
|
||||
if (selectedFragment != null) {
|
||||
selectedFragment.lastSeen.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -279,11 +288,17 @@ public class ConversationActivity extends XmppActivity {
|
|||
getSelectedConversation().getName(useSubject));
|
||||
invalidateOptionsMenu();
|
||||
if (!getSelectedConversation().isRead()) {
|
||||
xmppConnectionService.markRead(getSelectedConversation());
|
||||
xmppConnectionService
|
||||
.markRead(getSelectedConversation());
|
||||
UIHelper.updateNotification(getApplicationContext(),
|
||||
getConversationList(), null, false);
|
||||
listView.invalidateViews();
|
||||
}
|
||||
ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
|
||||
.findFragmentByTag("conversation");
|
||||
if ((selectedFragment != null) && (showLastseen())) {
|
||||
selectedFragment.lastSeen.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,7 +322,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
MenuItem menuInviteContacts = (MenuItem) menu
|
||||
.findItem(R.id.action_invite);
|
||||
MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
|
||||
MenuItem menuClearHistory = (MenuItem) menu.findItem(R.id.action_clear_history);
|
||||
MenuItem menuClearHistory = (MenuItem) menu
|
||||
.findItem(R.id.action_clear_history);
|
||||
|
||||
if ((spl.isOpen() && (spl.isSlideable()))) {
|
||||
menuArchive.setVisible(false);
|
||||
|
@ -344,19 +360,26 @@ public class ConversationActivity extends XmppActivity {
|
|||
public void onPresenceSelected(boolean success, String presence) {
|
||||
if (success) {
|
||||
if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) {
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, ImageProvider.getIncomingContentUri());
|
||||
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
|
||||
Intent takePictureIntent = new Intent(
|
||||
MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
|
||||
ImageProvider.getIncomingContentUri());
|
||||
if (takePictureIntent
|
||||
.resolveActivity(getPackageManager()) != null) {
|
||||
startActivityForResult(takePictureIntent,
|
||||
REQUEST_IMAGE_CAPTURE);
|
||||
}
|
||||
} else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
|
||||
Intent attachFileIntent = new Intent();
|
||||
attachFileIntent.setType("image/*");
|
||||
attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file));
|
||||
startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
|
||||
Intent chooser = Intent.createChooser(attachFileIntent,
|
||||
getString(R.string.attach_file));
|
||||
startActivityForResult(chooser,
|
||||
REQUEST_ATTACH_FILE_DIALOG);
|
||||
} else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
|
||||
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
|
||||
Intent intent = new Intent(
|
||||
MediaStore.Audio.Media.RECORD_SOUND_ACTION);
|
||||
startActivityForResult(intent, REQUEST_RECORD_AUDIO);
|
||||
}
|
||||
}
|
||||
|
@ -375,11 +398,13 @@ public class ConversationActivity extends XmppActivity {
|
|||
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
||||
if (hasPgp()) {
|
||||
if (conversation.getContact().getPgpKeyId() != 0) {
|
||||
xmppConnectionService.getPgpEngine().hasKey(conversation.getContact(), new UiCallback() {
|
||||
xmppConnectionService.getPgpEngine().hasKey(
|
||||
conversation.getContact(), new UiCallback() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi) {
|
||||
ConversationActivity.this.runIntent(pi, attachmentChoice);
|
||||
ConversationActivity.this.runIntent(pi,
|
||||
attachmentChoice);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -396,11 +421,14 @@ public class ConversationActivity extends XmppActivity {
|
|||
final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
|
||||
.findFragmentByTag("conversation");
|
||||
if (fragment != null) {
|
||||
fragment.showNoPGPKeyDialog(false,new OnClickListener() {
|
||||
fragment.showNoPGPKeyDialog(false,
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
selectPresenceToAttachFile(attachmentChoice);
|
||||
}
|
||||
});
|
||||
|
@ -415,20 +443,27 @@ public class ConversationActivity extends XmppActivity {
|
|||
builder.setMessage(getString(R.string.otr_file_transfer_msg));
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
if (conversation.getContact().getPgpKeyId() == 0) {
|
||||
builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() {
|
||||
builder.setPositiveButton(getString(R.string.send_unencrypted),
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
attachFile(attachmentChoice);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
builder.setPositiveButton(getString(R.string.use_pgp_encryption), new OnClickListener() {
|
||||
builder.setPositiveButton(
|
||||
getString(R.string.use_pgp_encryption),
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_PGP);
|
||||
attachFile(attachmentChoice);
|
||||
}
|
||||
});
|
||||
|
@ -447,7 +482,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
View menuAttachFile = findViewById(R.id.action_attach_file);
|
||||
PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
|
||||
attachFilePopup.inflate(R.menu.attachment_choices);
|
||||
attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
|
||||
attachFilePopup
|
||||
.setOnMenuItemClickListener(new OnMenuItemClickListener() {
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
|
@ -478,7 +514,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
if (contact.showInRoster()) {
|
||||
Intent intent = new Intent(this, ContactDetailsActivity.class);
|
||||
intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
|
||||
intent.putExtra("account", this.getSelectedConversation().getAccount().getJid());
|
||||
intent.putExtra("account", this.getSelectedConversation()
|
||||
.getAccount().getJid());
|
||||
intent.putExtra("contact", contact.getJid());
|
||||
startActivity(intent);
|
||||
} else {
|
||||
|
@ -511,25 +548,31 @@ public class ConversationActivity extends XmppActivity {
|
|||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.encryption_choice_none:
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
item.setChecked(true);
|
||||
break;
|
||||
case R.id.encryption_choice_otr:
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_OTR);
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_OTR);
|
||||
item.setChecked(true);
|
||||
break;
|
||||
case R.id.encryption_choice_pgp:
|
||||
if (hasPgp()) {
|
||||
if (conversation.getAccount().getKeys().has("pgp_signature")) {
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
|
||||
if (conversation.getAccount().getKeys()
|
||||
.has("pgp_signature")) {
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_PGP);
|
||||
item.setChecked(true);
|
||||
} else {
|
||||
announcePgp(conversation.getAccount(),conversation);
|
||||
announcePgp(conversation.getAccount(),
|
||||
conversation);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
break;
|
||||
}
|
||||
fragment.updateChatMsgHint();
|
||||
|
@ -537,7 +580,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
});
|
||||
popup.inflate(R.menu.encryption_choices);
|
||||
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
|
||||
MenuItem otr = popup.getMenu().findItem(
|
||||
R.id.encryption_choice_otr);
|
||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
otr.setEnabled(false);
|
||||
}
|
||||
|
@ -586,15 +630,19 @@ public class ConversationActivity extends XmppActivity {
|
|||
protected void clearHistoryDialog(final Conversation conversation) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.clear_conversation_history));
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
|
||||
final CheckBox endConversationCheckBox = (CheckBox) dialogView.findViewById(R.id.end_conversation_checkbox);
|
||||
View dialogView = getLayoutInflater().inflate(
|
||||
R.layout.dialog_clear_history, null);
|
||||
final CheckBox endConversationCheckBox = (CheckBox) dialogView
|
||||
.findViewById(R.id.end_conversation_checkbox);
|
||||
builder.setView(dialogView);
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setPositiveButton(getString(R.string.delete_messages), new OnClickListener() {
|
||||
builder.setPositiveButton(getString(R.string.delete_messages),
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
activity.xmppConnectionService.clearConversationHistory(conversation);
|
||||
activity.xmppConnectionService
|
||||
.clearConversationHistory(conversation);
|
||||
if (endConversationCheckBox.isChecked()) {
|
||||
endConversation(conversation);
|
||||
}
|
||||
|
@ -627,9 +675,9 @@ public class ConversationActivity extends XmppActivity {
|
|||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if ((Intent.ACTION_VIEW.equals(intent.getAction())&&(VIEW_CONVERSATION.equals(intent.getType())))) {
|
||||
String convToView = (String) intent.getExtras().get(
|
||||
CONVERSATION);
|
||||
if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
|
||||
.equals(intent.getType())))) {
|
||||
String convToView = (String) intent.getExtras().get(CONVERSATION);
|
||||
updateConversationList();
|
||||
for (int i = 0; i < conversationList.size(); ++i) {
|
||||
if (conversationList.get(i).getUuid().equals(convToView)) {
|
||||
|
@ -649,6 +697,7 @@ public class ConversationActivity extends XmppActivity {
|
|||
SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
|
||||
this.showLastseen = preferences.getBoolean("show_last_seen", false);
|
||||
if (this.xmppConnectionServiceBound) {
|
||||
this.onBackendConnected();
|
||||
}
|
||||
|
@ -722,7 +771,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
|
||||
protected void onActivityResult(int requestCode, int resultCode,
|
||||
final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (requestCode == REQUEST_DECRYPT_PGP) {
|
||||
|
@ -732,7 +782,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
selectedFragment.hidePgpPassphraseBox();
|
||||
}
|
||||
} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
|
||||
attachImageToConversation(getSelectedConversation(),data.getData());
|
||||
attachImageToConversation(getSelectedConversation(),
|
||||
data.getData());
|
||||
} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
|
||||
|
||||
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
|
||||
|
@ -740,14 +791,16 @@ public class ConversationActivity extends XmppActivity {
|
|||
} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
|
||||
attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
|
||||
} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
|
||||
announcePgp(getSelectedConversation().getAccount(),getSelectedConversation());
|
||||
announcePgp(getSelectedConversation().getAccount(),
|
||||
getSelectedConversation());
|
||||
} else if (requestCode == REQUEST_ENCRYPT_MESSAGE) {
|
||||
encryptTextMessage();
|
||||
} else if (requestCode == REQUEST_IMAGE_CAPTURE) {
|
||||
attachImageToConversation(getSelectedConversation(), null);
|
||||
} else if (requestCode == REQUEST_RECORD_AUDIO) {
|
||||
Log.d("xmppService", data.getData().toString());
|
||||
attachAudioToConversation(getSelectedConversation(),data.getData());
|
||||
attachAudioToConversation(getSelectedConversation(),
|
||||
data.getData());
|
||||
} else {
|
||||
Log.d(LOGTAG, "unknown result code:" + requestCode);
|
||||
}
|
||||
|
@ -759,14 +812,17 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
|
||||
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
||||
prepareImageToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
|
||||
prepareImageToast = Toast.makeText(getApplicationContext(),
|
||||
getText(R.string.preparing_image), Toast.LENGTH_LONG);
|
||||
prepareImageToast.show();
|
||||
pendingMessage = xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback() {
|
||||
pendingMessage = xmppConnectionService.attachImageToConversation(
|
||||
conversation, uri, new UiCallback() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi) {
|
||||
hidePrepareImageToast();
|
||||
ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_PGP_IMAGE);
|
||||
ConversationActivity.this.runIntent(pi,
|
||||
ConversationActivity.REQUEST_SEND_PGP_IMAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -810,25 +866,30 @@ public class ConversationActivity extends XmppActivity {
|
|||
listView.invalidateViews();
|
||||
}
|
||||
|
||||
public void selectPresence(final Conversation conversation, final OnPresenceSelected listener, String reason) {
|
||||
public void selectPresence(final Conversation conversation,
|
||||
final OnPresenceSelected listener, String reason) {
|
||||
Account account = conversation.getAccount();
|
||||
if (account.getStatus() != Account.STATUS_ONLINE) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.not_connected));
|
||||
builder.setIconAttribute(android.R.attr.alertDialogIcon);
|
||||
if ("otr".equals(reason)) {
|
||||
builder.setMessage(getString(R.string.you_are_offline,getString(R.string.otr_messages)));
|
||||
builder.setMessage(getString(R.string.you_are_offline,
|
||||
getString(R.string.otr_messages)));
|
||||
} else if ("file".equals(reason)) {
|
||||
builder.setMessage(getString(R.string.you_are_offline,getString(R.string.files)));
|
||||
builder.setMessage(getString(R.string.you_are_offline,
|
||||
getString(R.string.files)));
|
||||
} else {
|
||||
builder.setMessage(getString(R.string.you_are_offline_blank));
|
||||
}
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setPositiveButton(getString(R.string.manage_account), new OnClickListener() {
|
||||
builder.setPositiveButton(getString(R.string.manage_account),
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
startActivity(new Intent(activity, ManageAccountActivity.class));
|
||||
startActivity(new Intent(activity,
|
||||
ManageAccountActivity.class));
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
|
@ -845,10 +906,13 @@ public class ConversationActivity extends XmppActivity {
|
|||
builder.setTitle(getString(R.string.contact_offline));
|
||||
if ("otr".equals(reason)) {
|
||||
builder.setMessage(getString(R.string.contact_offline_otr));
|
||||
builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() {
|
||||
builder.setPositiveButton(
|
||||
getString(R.string.send_unencrypted),
|
||||
new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
listener.onSendPlainTextInstead();
|
||||
}
|
||||
});
|
||||
|
@ -885,18 +949,29 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean showLastseen() {
|
||||
if (getSelectedConversation() == null) {
|
||||
return false;
|
||||
} else {
|
||||
return this.showLastseen
|
||||
&& getSelectedConversation().getMode() == Conversation.MODE_SINGLE;
|
||||
}
|
||||
}
|
||||
|
||||
private void showAddToRosterDialog(final Conversation conversation) {
|
||||
String jid = conversation.getContactJid();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(jid);
|
||||
builder.setMessage(getString(R.string.not_in_roster));
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setPositiveButton(getString(R.string.add_contact), new DialogInterface.OnClickListener() {
|
||||
builder.setPositiveButton(getString(R.string.add_contact),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String jid = conversation.getContactJid();
|
||||
Account account = getSelectedConversation().getAccount();
|
||||
Account account = getSelectedConversation()
|
||||
.getAccount();
|
||||
Contact contact = account.getRoster().getContact(jid);
|
||||
xmppConnectionService.createContact(contact);
|
||||
}
|
||||
|
@ -906,14 +981,13 @@ public class ConversationActivity extends XmppActivity {
|
|||
|
||||
public void runIntent(PendingIntent pi, int requestCode) {
|
||||
try {
|
||||
this.startIntentSenderForResult(pi.getIntentSender(),requestCode, null, 0,
|
||||
0, 0);
|
||||
this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
|
||||
null, 0, 0, 0);
|
||||
} catch (SendIntentException e1) {
|
||||
Log.d("xmppService", "failed to start intent to send message");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
|
||||
private final WeakReference<ImageView> imageViewReference;
|
||||
private Message message = null;
|
||||
|
@ -926,7 +1000,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
protected Bitmap doInBackground(Message... params) {
|
||||
message = params[0];
|
||||
try {
|
||||
return xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288),false);
|
||||
return xmppConnectionService.getFileBackend().getThumbnail(
|
||||
message, (int) (metrics.density * 288), false);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d("xmppService", "file not found!");
|
||||
return null;
|
||||
|
@ -948,7 +1023,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
public void loadBitmap(Message message, ImageView imageView) {
|
||||
Bitmap bm;
|
||||
try {
|
||||
bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
|
||||
bm = xmppConnectionService.getFileBackend().getThumbnail(message,
|
||||
(int) (metrics.density * 288), true);
|
||||
} catch (FileNotFoundException e) {
|
||||
bm = null;
|
||||
}
|
||||
|
@ -959,15 +1035,16 @@ public class ConversationActivity extends XmppActivity {
|
|||
if (cancelPotentialWork(message, imageView)) {
|
||||
imageView.setBackgroundColor(0xff333333);
|
||||
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
|
||||
final AsyncDrawable asyncDrawable =
|
||||
new AsyncDrawable(getResources(), null, task);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(
|
||||
getResources(), null, task);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
task.execute(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean cancelPotentialWork(Message message, ImageView imageView) {
|
||||
public static boolean cancelPotentialWork(Message message,
|
||||
ImageView imageView) {
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
|
||||
if (bitmapWorkerTask != null) {
|
||||
|
@ -998,8 +1075,8 @@ public class ConversationActivity extends XmppActivity {
|
|||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||
BitmapWorkerTask bitmapWorkerTask) {
|
||||
super(res, bitmap);
|
||||
bitmapWorkerTaskReference =
|
||||
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
||||
bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
|
||||
bitmapWorkerTask);
|
||||
}
|
||||
|
||||
public BitmapWorkerTask getBitmapWorkerTask() {
|
||||
|
@ -1008,13 +1085,12 @@ public class ConversationActivity extends XmppActivity {
|
|||
}
|
||||
|
||||
public void encryptTextMessage() {
|
||||
xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage, new UiCallback() {
|
||||
xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage,
|
||||
new UiCallback() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(
|
||||
PendingIntent pi) {
|
||||
activity.runIntent(
|
||||
pi,
|
||||
public void userInputRequried(PendingIntent pi) {
|
||||
activity.runIntent(pi,
|
||||
ConversationActivity.REQUEST_SEND_MESSAGE);
|
||||
}
|
||||
|
||||
|
|
|
@ -107,7 +107,9 @@ public class ConversationFragment extends Fragment {
|
|||
|
||||
private LinearLayout pgpInfo;
|
||||
private LinearLayout mucError;
|
||||
public LinearLayout lastSeen;
|
||||
private TextView mucErrorText;
|
||||
private TextView lastSeenText;
|
||||
private OnClickListener clickToMuc = new OnClickListener() {
|
||||
|
||||
@Override
|
||||
|
@ -161,6 +163,8 @@ public class ConversationFragment extends Fragment {
|
|||
mucError = (LinearLayout) view.findViewById(R.id.muc_error);
|
||||
mucError.setOnClickListener(clickToMuc);
|
||||
mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
|
||||
lastSeen = (LinearLayout) view.findViewById(R.id.last_seen);
|
||||
lastSeenText = (TextView) view.findViewById(R.id.last_seen_text);
|
||||
|
||||
messagesView = (ListView) view.findViewById(R.id.messages_view);
|
||||
|
||||
|
@ -335,15 +339,18 @@ public class ConversationFragment extends Fragment {
|
|||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
|
||||
viewHolder.image
|
||||
.setOnLongClickListener(new OnLongClickListener() {
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
Intent shareIntent = new Intent();
|
||||
shareIntent.setAction(Intent.ACTION_SEND);
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, ImageProvider.getContentUri(message));
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM,
|
||||
ImageProvider.getContentUri(message));
|
||||
shareIntent.setType("image/webp");
|
||||
startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
|
||||
startActivity(Intent.createChooser(shareIntent,
|
||||
getText(R.string.share_with)));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
@ -405,8 +412,8 @@ public class ConversationFragment extends Fragment {
|
|||
view.setTag(viewHolder);
|
||||
break;
|
||||
case STATUS:
|
||||
view = (View) inflater.inflate(
|
||||
R.layout.message_status, null);
|
||||
view = (View) inflater.inflate(R.layout.message_status,
|
||||
null);
|
||||
viewHolder.contact_picture = (ImageView) view
|
||||
.findViewById(R.id.message_photo);
|
||||
if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
|
||||
|
@ -577,7 +584,11 @@ public class ConversationFragment extends Fragment {
|
|||
activity.getActionBar().setTitle(
|
||||
conversation.getName(useSubject));
|
||||
activity.invalidateOptionsMenu();
|
||||
|
||||
if (activity.showLastseen()) {
|
||||
lastSeen.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
lastSeen.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
|
@ -653,6 +664,16 @@ public class ConversationFragment extends Fragment {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (activity.showLastseen()) {
|
||||
Contact contact = conversation.getContact();
|
||||
if ((contact.lastseen.presence != null)&&(contact.lastseen.time != 0)) {
|
||||
lastSeenText.setText(getString(R.string.last_seen,
|
||||
UIHelper.lastseen(getActivity(), contact.lastseen.time),
|
||||
contact.lastseen.presence));
|
||||
} else {
|
||||
lastSeenText.setText(R.string.never_seen);
|
||||
}
|
||||
}
|
||||
this.messageList.clear();
|
||||
this.messageList.addAll(this.conversation.getMessages());
|
||||
updateStatusMessages();
|
||||
|
@ -693,7 +714,8 @@ public class ConversationFragment extends Fragment {
|
|||
return;
|
||||
} else {
|
||||
if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
|
||||
this.messageList.add(i+1, Message.createStatusMessage(conversation));
|
||||
this.messageList.add(i + 1,
|
||||
Message.createStatusMessage(conversation));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,19 @@ public class UIHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static String lastseen(Context context, long time) {
|
||||
long difference = (System.currentTimeMillis() - time) / 1000;
|
||||
if (difference < 60) {
|
||||
return context.getString(R.string.just_now);
|
||||
} else if (difference < 60 * 60) {
|
||||
return difference / 60 + " " + context.getString(R.string.mins);
|
||||
} else if (difference < 60 * 60 * 24) {
|
||||
return difference / (60 * 60)+ " " + context.getString(R.string.hours);
|
||||
} else {
|
||||
return "days";
|
||||
}
|
||||
}
|
||||
|
||||
public static int getRealPx(int dp, Context context) {
|
||||
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return ((int) (dp * metrics.density));
|
||||
|
|
Loading…
Reference in a new issue