forked from mirror/monocles_chat_clean
update fork #128
135 changed files with 8599 additions and 811 deletions
|
|
@ -32,8 +32,7 @@ Il existe également un Support-MUC où vous pouvez poser des questions et obten
|
|||
|
||||
|
||||
## Comment puis-je aider à la traduction ?
|
||||
Tu peux améliorer ou créer des traductions sur <a href="https://translate.codeberg.org/projects/monocles_chat/">Codeberg Translate</a>. Merci beaucoup.
|
||||
|
||||
Vous pouvez améliorer ou créer des traductions sur <a href="https://translate.codeberg.org/projects/monocles_chat/">Codeberg Translate</a>. Merci beaucoup.
|
||||
|
||||
|
||||
## Aidez-moi ! J'ai rencontré des problèmes !
|
||||
|
|
|
|||
14
build.gradle
14
build.gradle
|
|
@ -116,6 +116,11 @@ dependencies {
|
|||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
implementation 'net.fellbaum:jemoji:1.4.1'
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1' // For photo editor compatibility
|
||||
implementation('com.github.bumptech.glide:okhttp3-integration:4.15.1') {
|
||||
exclude group: 'glide-parent'
|
||||
}
|
||||
implementation 'com.github.bumptech.glide:annotations:4.15.1'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
|
||||
|
||||
implementation 'com.github.natario1:Autocomplete:v1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
|
|
@ -128,6 +133,7 @@ dependencies {
|
|||
implementation "androidx.media3:media3-ui:1.3.1"
|
||||
implementation "androidx.media3:media3-session:1.3.1"
|
||||
implementation 'com.kizitonwose.calendar:view:2.5.4'
|
||||
implementation "io.noties.markwon:core:4.6.2"
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
@ -142,8 +148,8 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 195
|
||||
versionName "2.0.18"
|
||||
versionCode 196
|
||||
versionName "2.1"
|
||||
applicationId "de.monocles.chat"
|
||||
def appName = "monocles chat"
|
||||
resValue "string", "app_name", appName
|
||||
|
|
@ -209,8 +215,8 @@ android {
|
|||
monocleschat {
|
||||
dimension "mode"
|
||||
applicationId = "de.monocles.chat"
|
||||
versionCode 195
|
||||
versionName "2.0.18"
|
||||
versionCode 196
|
||||
versionName "2.1"
|
||||
def appName = "monocles chat"
|
||||
resValue "string", "app_name", appName
|
||||
buildConfigField "String", "APP_NAME", "\"$appName\""
|
||||
|
|
|
|||
|
|
@ -567,5 +567,26 @@
|
|||
<xmpp:note xml:lang='en'>Receiving retractions</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0501.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.2.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0277.html"/>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:version>0.6.5</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0472.html"/>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:version>0.2.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
</Project>
|
||||
</rdf:RDF>
|
||||
|
|
|
|||
|
|
@ -396,6 +396,12 @@
|
|||
<activity
|
||||
android:name=".ui.MucUsersActivity"
|
||||
android:label="@string/group_chat_members" />
|
||||
<activity
|
||||
android:name=".ui.StoriesActivity"
|
||||
android:label="@string/stories" />
|
||||
<activity
|
||||
android:name=".ui.StoryViewActivity"
|
||||
android:label="@string/story" />
|
||||
<activity
|
||||
android:name=".ui.ChannelDiscoveryActivity"
|
||||
android:label="@string/discover_channels" />
|
||||
|
|
@ -462,6 +468,14 @@
|
|||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.CallsActivity"
|
||||
android:label="@string/calls" />
|
||||
<activity
|
||||
android:name=".ui.PostsActivity"
|
||||
android:label="@string/feeds" />
|
||||
<activity
|
||||
android:name=".ui.CreatePostActivity"
|
||||
android:label="@string/create_post" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class AppSettings {
|
|||
|
||||
public static final String TRUST_SYSTEM_CA_STORE = "trust_system_ca_store";
|
||||
public static final String DANE_ENFORCED = "enforce_dane";
|
||||
public static final String CUSTOM_RESOURCE_NAME = "custom_resource_name";
|
||||
public static final String REQUIRE_CHANNEL_BINDING = "channel_binding_required";
|
||||
public static final String REQUIRE_TLS_V1_3 = "require_tls_v1_3";
|
||||
public static final String NOTIFICATION_RINGTONE = "notification_ringtone";
|
||||
|
|
|
|||
77
src/main/java/eu/siacs/conversations/CustomGlideModule.java
Normal file
77
src/main/java/eu/siacs/conversations/CustomGlideModule.java
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package eu.siacs.conversations;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
@GlideModule
|
||||
public class CustomGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
|
||||
// Start with the app's base OkHttpClient, which is already configured with the app's custom trust managers.
|
||||
// This is the crucial step that makes direct connections (without a proxy) work correctly.
|
||||
final OkHttpClient baseClient = HttpConnectionManager.okHttpClient(context);
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
// Create a dynamic ProxySelector. This selector's 'select' method will be called
|
||||
// for each new network request, so it will always use the most current proxy settings.
|
||||
ProxySelector proxySelector = new ProxySelector() {
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
final boolean useI2p = preferences.getBoolean("use_i2p", false);
|
||||
if (useI2p) {
|
||||
return Collections.singletonList(HttpConnectionManager.getProxy(true));
|
||||
}
|
||||
final boolean useTor = preferences.getBoolean("use_tor", false);
|
||||
if (useTor) {
|
||||
return Collections.singletonList(HttpConnectionManager.getProxy(false));
|
||||
}
|
||||
|
||||
// When no custom proxy is active, it is VITAL to return Proxy.NO_PROXY.
|
||||
// Returning null or an empty list would cause OkHttp to fall back to system-wide proxies,
|
||||
// which we want to avoid. This forces a direct connection using our
|
||||
// correctly configured (and trusted) OkHttpClient.
|
||||
return Collections.singletonList(Proxy.NO_PROXY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
// This method is called if a connection to a proxy fails.
|
||||
// You could add logging here for debugging if needed.
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new client by cloning the base client and setting our dynamic proxy selector.
|
||||
final OkHttpClient clientWithProxy = baseClient.newBuilder()
|
||||
.proxySelector(proxySelector)
|
||||
.build();
|
||||
|
||||
final OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(clientWithProxy);
|
||||
|
||||
registry.replace(GlideUrl.class, InputStream.class, factory);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
|
||||
public static final String FAST_MECHANISM = "fast_mechanism";
|
||||
public static final String FAST_TOKEN = "fast_token";
|
||||
public static final String ORDERING = "ordering";
|
||||
|
||||
private int ordering = 0;
|
||||
public static final int OPTION_DISABLED = 1;
|
||||
|
|
@ -146,7 +147,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
null,
|
||||
0);
|
||||
}
|
||||
|
||||
private Account(
|
||||
|
|
@ -165,7 +167,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
final String pinnedMechanism,
|
||||
final String pinnedChannelBinding,
|
||||
final String fastMechanism,
|
||||
final String fastToken) {
|
||||
final String fastToken,
|
||||
final int ordering) {
|
||||
this.uuid = uuid;
|
||||
this.jid = jid;
|
||||
this.password = password;
|
||||
|
|
@ -182,6 +185,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
this.pinnedChannelBinding = pinnedChannelBinding;
|
||||
this.fastMechanism = fastMechanism;
|
||||
this.fastToken = fastToken;
|
||||
this.ordering = ordering;
|
||||
}
|
||||
|
||||
public static JSONObject parseKeys(final String keys) {
|
||||
|
|
@ -229,7 +233,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(ORDERING)));
|
||||
}
|
||||
|
||||
public void setMamPrefs(Element prefs) {
|
||||
|
|
@ -590,6 +595,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding);
|
||||
values.put(FAST_MECHANISM, this.fastMechanism);
|
||||
values.put(FAST_TOKEN, this.fastToken);
|
||||
values.put(ORDERING, ordering);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
|
@ -746,7 +752,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
|
||||
public boolean areBookmarksLoaded() {
|
||||
// No way to tell if old PEP bookmarks are all loaded yet if they are empty
|
||||
// because we don't manually fetch them...
|
||||
// because we don\'t manually fetch them...
|
||||
if (getXmppConnection().getFeatures().bookmarksConversion()) return true;
|
||||
|
||||
return bookmarksLoaded;
|
||||
|
|
|
|||
59
src/main/java/eu/siacs/conversations/entities/Call.java
Normal file
59
src/main/java/eu/siacs/conversations/entities/Call.java
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
|
||||
package eu.siacs.conversations.entities;
|
||||
|
||||
import eu.siacs.conversations.services.AvatarService;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class Call implements AvatarService.Avatarable {
|
||||
|
||||
private final String contact;
|
||||
private final Jid jid;
|
||||
private final long startTime;
|
||||
private final int status;
|
||||
private final boolean isVideoCall;
|
||||
private final boolean successful;
|
||||
|
||||
public Call(String contact, Jid jid, long startTime, int status, boolean isVideoCall, boolean successful) {
|
||||
this.contact = contact;
|
||||
this.jid = jid;
|
||||
this.startTime = startTime;
|
||||
this.status = status;
|
||||
this.isVideoCall = isVideoCall;
|
||||
this.successful = successful;
|
||||
}
|
||||
|
||||
public String getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public Jid getJid() {
|
||||
return jid;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isVideoCall() {
|
||||
return isVideoCall;
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
return successful;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvatarBackgroundColor() {
|
||||
return UIHelper.getColorForName(getContact());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarName() {
|
||||
return getContact();
|
||||
}
|
||||
}
|
||||
79
src/main/java/eu/siacs/conversations/entities/Comment.java
Normal file
79
src/main/java/eu/siacs/conversations/entities/Comment.java
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import static eu.siacs.conversations.parser.AbstractParser.parseTimestamp;
|
||||
import static eu.siacs.conversations.parser.AbstractParser.parseTimestampAtom;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import eu.siacs.conversations.parser.AbstractParser;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class Comment {
|
||||
|
||||
private final String id;
|
||||
private final String title;
|
||||
private final Jid author;
|
||||
private final Date published;
|
||||
|
||||
public Comment(String id, String title, Jid author, Date published) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.published = published;
|
||||
}
|
||||
|
||||
public static Comment fromElement(Element item) {
|
||||
Element entry = item.findChild("entry", "http://www.w3.org/2005/Atom");
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
String id = item.getAttribute("id");
|
||||
String title = entry.findChildContent("title");
|
||||
Element authorElement = entry.findChild("author");
|
||||
Jid author = null;
|
||||
if (authorElement != null) {
|
||||
String uri = authorElement.findChildContent("uri");
|
||||
if (uri != null && uri.startsWith("xmpp:")) {
|
||||
try {
|
||||
author = Jid.of(uri.substring(5));
|
||||
} catch (IllegalArgumentException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
Date published = null;
|
||||
String publishedString = entry.findChildContent("published");
|
||||
if (publishedString != null) {
|
||||
try {
|
||||
published = new Date(parseTimestamp(publishedString));
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
published = new Date(parseTimestampAtom(publishedString));
|
||||
} catch (ParseException error) {
|
||||
Log.e("Feeds", "Couldn't parse timestamp " + publishedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Comment(id, title, author, published);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Jid getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public Date getPublished() {
|
||||
return published;
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ public class Contact implements ListItem, Blockable {
|
|||
public static final String LAST_TIME = "last_time";
|
||||
public static final String GROUPS = "groups";
|
||||
public static final String RTP_CAPABILITY = "rtpCapability";
|
||||
public static final String CALLS_DISABLED = "callsDisabled";
|
||||
private String accountUuid;
|
||||
private String systemName;
|
||||
private String serverName;
|
||||
|
|
@ -84,9 +85,10 @@ public class Contact implements ListItem, Blockable {
|
|||
private String mLastPresence = null;
|
||||
private UserTune mUserTune = null;
|
||||
private RtpCapability.Capability rtpCapability;
|
||||
private boolean callsDisabled;
|
||||
|
||||
public Contact(Contact other) {
|
||||
this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar() == null ? null : other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability);
|
||||
this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar() == null ? null : other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability, other.callsDisabled);
|
||||
setAccount(other.getAccount());
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +106,8 @@ public class Contact implements ListItem, Blockable {
|
|||
final long lastseen,
|
||||
final String presence,
|
||||
final String groups,
|
||||
final RtpCapability.Capability rtpCapability) {
|
||||
final RtpCapability.Capability rtpCapability,
|
||||
final boolean callsDisabled) {
|
||||
this.accountUuid = account;
|
||||
this.systemName = systemName;
|
||||
this.serverName = serverName;
|
||||
|
|
@ -133,6 +136,7 @@ public class Contact implements ListItem, Blockable {
|
|||
this.mLastseen = lastseen;
|
||||
this.mLastPresence = presence;
|
||||
this.rtpCapability = rtpCapability;
|
||||
this.callsDisabled = callsDisabled;
|
||||
}
|
||||
|
||||
public Contact(final Jid jid) {
|
||||
|
|
@ -169,7 +173,8 @@ public class Contact implements ListItem, Blockable {
|
|||
cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
|
||||
cursor.getString(cursor.getColumnIndex(GROUPS)),
|
||||
RtpCapability.Capability.of(
|
||||
cursor.getString(cursor.getColumnIndex(RTP_CAPABILITY))));
|
||||
cursor.getString(cursor.getColumnIndex(RTP_CAPABILITY))),
|
||||
cursor.getInt(cursor.getColumnIndex(CALLS_DISABLED)) > 0);
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
|
|
@ -295,6 +300,7 @@ public class Contact implements ListItem, Blockable {
|
|||
values.put(LAST_TIME, mLastseen);
|
||||
values.put(GROUPS, groups.toString());
|
||||
values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
|
||||
values.put(CALLS_DISABLED, callsDisabled ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
|
@ -815,9 +821,20 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public RtpCapability.Capability getRtpCapability() {
|
||||
if (this.callsDisabled) {
|
||||
return null;
|
||||
}
|
||||
return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
|
||||
}
|
||||
|
||||
public void setCallsDisabled(boolean callsDisabled) {
|
||||
this.callsDisabled = callsDisabled;
|
||||
}
|
||||
|
||||
public boolean areCallsDisabled() {
|
||||
return callsDisabled;
|
||||
}
|
||||
|
||||
public static final class Options {
|
||||
public static final int TO = 0;
|
||||
public static final int FROM = 1;
|
||||
|
|
@ -829,10 +846,23 @@ public class Contact implements ListItem, Blockable {
|
|||
public static final int DIRTY_DELETE = 7;
|
||||
private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
|
||||
public static final int SYNCED_VIA_OTHER = 9;
|
||||
public static final int FOLLOWED = 10;
|
||||
}
|
||||
|
||||
// Method to update this Contact's presences/status messages
|
||||
public void updatePresences(List<String> newStatusMessages) {
|
||||
this.presences.updateStatusMessages(newStatusMessages);
|
||||
}
|
||||
|
||||
public boolean isFollowed() {
|
||||
return getOption(Options.FOLLOWED);
|
||||
}
|
||||
|
||||
public void setFollowed(boolean followed) {
|
||||
if (followed) {
|
||||
setOption(Options.FOLLOWED);
|
||||
} else {
|
||||
resetOption(Options.FOLLOWED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ public class Conversation extends AbstractEntity
|
|||
protected boolean lockThread = false;
|
||||
protected boolean userSelectedThread = false;
|
||||
protected Message replyTo = null;
|
||||
protected Message caption = null;
|
||||
protected String caption = null;
|
||||
protected HashMap<String, Thread> threads = new HashMap<>();
|
||||
protected Multimap<String, Reaction> reactions = HashMultimap.create();
|
||||
private String displayState = null;
|
||||
|
|
@ -1060,11 +1060,11 @@ public class Conversation extends AbstractEntity
|
|||
return this.replyTo;
|
||||
}
|
||||
|
||||
public void setCaption(Message m) {
|
||||
this.caption = m;
|
||||
public void setCaption(String caption) {
|
||||
this.caption = caption;
|
||||
}
|
||||
|
||||
public Message getCaption() {
|
||||
public String getCaption() {
|
||||
return this.caption;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final int TYPE_PRIVATE = 4;
|
||||
public static final int TYPE_PRIVATE_FILE = 5;
|
||||
public static final int TYPE_RTP_SESSION = 6;
|
||||
public static final int TYPE_STORY = 7;
|
||||
|
||||
public static final String CONVERSATION = "conversationUuid";
|
||||
public static final String COUNTERPART = "counterpart";
|
||||
|
|
@ -188,6 +189,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
private List<MucOptions.User> counterparts;
|
||||
private WeakReference<MucOptions.User> user;
|
||||
private String retractId = null;
|
||||
private androidx.core.util.Pair<Jid, String> storyReference = null;
|
||||
|
||||
protected Message(Conversational conversation) {
|
||||
this.conversation = conversation;
|
||||
|
|
@ -275,7 +277,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
final long timeSent,
|
||||
final int encryption,
|
||||
final int status,
|
||||
final int type,
|
||||
int type,
|
||||
final boolean carbon,
|
||||
final String remoteMsgId,
|
||||
final String relativeFilePath,
|
||||
|
|
@ -322,6 +324,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
if (payloads != null) this.payloads = payloads;
|
||||
if (fileParams != null && getSims().isEmpty()) this.fileParams = new FileParams(fileParams);
|
||||
this.retractId = retractId;
|
||||
|
||||
if (type == TYPE_TEXT || type == TYPE_PRIVATE) {
|
||||
final FileParams fp = getFileParams();
|
||||
if (fp != null && fp.url != null && fp.url.startsWith("xmpp:")) {
|
||||
try {
|
||||
final android.net.Uri uri = android.net.Uri.parse(fp.url);
|
||||
String schemeSpecificPart = uri.getSchemeSpecificPart();
|
||||
int queryStart = schemeSpecificPart.indexOf('?');
|
||||
if (queryStart != -1) {
|
||||
String queryString = schemeSpecificPart.substring(queryStart + 1);
|
||||
String[] params = queryString.split(";");
|
||||
for (String param : params) {
|
||||
String[] keyValue = param.split("=", 2);
|
||||
if (keyValue.length == 2 && "node".equals(keyValue[0]) && "urn:xmpp:pubsub-social-feed:stories:0".equals(keyValue[1])) {
|
||||
type = TYPE_STORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// not a story URI
|
||||
}
|
||||
}
|
||||
}
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
|
||||
|
|
@ -377,6 +404,44 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
return m;
|
||||
}
|
||||
|
||||
public androidx.core.util.Pair<Jid, String> getStoryReference() {
|
||||
if (this.storyReference != null) {
|
||||
return this.storyReference;
|
||||
}
|
||||
final FileParams fp = getFileParams();
|
||||
if (fp == null || fp.url == null || !fp.url.startsWith("xmpp:")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final String uriString = fp.url;
|
||||
final android.net.Uri uri = android.net.Uri.parse(uriString);
|
||||
String schemeSpecificPart = uri.getSchemeSpecificPart();
|
||||
int queryStart = schemeSpecificPart.indexOf('?');
|
||||
if (queryStart == -1) {
|
||||
return null;
|
||||
}
|
||||
String jidString = schemeSpecificPart.substring(0, queryStart);
|
||||
final Jid jid = Jid.of(jidString);
|
||||
String queryString = schemeSpecificPart.substring(queryStart + 1);
|
||||
String item = null;
|
||||
String[] params = queryString.split(";");
|
||||
for (String param : params) {
|
||||
String[] keyValue = param.split("=", 2);
|
||||
if (keyValue.length == 2 && "item".equals(keyValue[0])) {
|
||||
item = keyValue[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (item != null) {
|
||||
this.storyReference = new androidx.core.util.Pair<>(jid, item);
|
||||
return this.storyReference;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Not a valid story URI
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Jid fromString(String value) {
|
||||
try {
|
||||
if (value != null) {
|
||||
|
|
@ -491,7 +556,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
clearReplyReact();
|
||||
|
||||
if (body == null) body = new SpannableStringBuilder(getBody(false));
|
||||
setBody(QuoteHelper.quote(MessageUtils.prepareQuote(replyTo)) + "\n");
|
||||
setBody(QuoteHelper.quote(MessageUtils.prepareQuote(replyTo)) + "\n\n");
|
||||
|
||||
final String replyId = replyTo.replyId();
|
||||
if (replyId == null) return;
|
||||
|
|
@ -1190,7 +1255,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
|
||||
if (includeReplyTo && getInReplyTo() != null && getModerated() == null) {
|
||||
// Don't show quote if it's the message right before us
|
||||
if (prev() != null && prev().getUuid().equals(getInReplyTo().getUuid())) return spannableBody;
|
||||
// if (prev() != null && prev().getUuid().equals(getInReplyTo().getUuid())) return spannableBody;
|
||||
|
||||
final var quote = getInReplyTo().getSpannableBody(thumbnailer, fallbackImg);
|
||||
if ((getInReplyTo().isFileOrImage() || getInReplyTo().isOOb()) && getInReplyTo().getFileParams() != null) {
|
||||
|
|
|
|||
178
src/main/java/eu/siacs/conversations/entities/Post.java
Normal file
178
src/main/java/eu/siacs/conversations/entities/Post.java
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package eu.siacs.conversations.entities;import static eu.siacs.conversations.parser.AbstractParser.parseTimestamp;
|
||||
import static eu.siacs.conversations.parser.AbstractParser.parseTimestampAtom;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class Post {
|
||||
|
||||
public static final String TABLENAME = "posts";
|
||||
public static final String UUID = "uuid";
|
||||
public static final String ACCOUNT_UUID = "account_uuid";
|
||||
public static final String AUTHOR_JID = "author_jid";
|
||||
public static final String TITLE = "title";
|
||||
public static final String CONTENT = "content";
|
||||
public static final String ATTACHMENT_URL = "attachment_url";
|
||||
public static final String ATTACHMENT_TYPE = "attachment_type";
|
||||
public static final String PUBLISHED = "published";
|
||||
public static final String COMMENTS_NODE = "comments_node";
|
||||
|
||||
private final String id;
|
||||
private final String title;
|
||||
private final String content;
|
||||
private final Jid author;
|
||||
private final Date published;
|
||||
private final String commentsNode;
|
||||
private final String attachmentUrl;
|
||||
private final String attachmentType;
|
||||
|
||||
public Post(String id, String title, String content, Jid author, Date published, String commentsNode, String attachmentUrl, String attachmentType) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.author = author;
|
||||
this.published = published;
|
||||
this.commentsNode = commentsNode;
|
||||
this.attachmentUrl = attachmentUrl;
|
||||
this.attachmentType = attachmentType;
|
||||
}
|
||||
|
||||
public static Post fromElement(Element item) {
|
||||
final Element entry = item.findChild("entry", Namespace.ATOM);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
String id = item.getAttribute("id");
|
||||
String title = entry.findChildContent("title");
|
||||
String content = entry.findChildContent("content");
|
||||
Element authorElement = entry.findChild("author");
|
||||
Jid author = null;
|
||||
if (authorElement != null) {
|
||||
String uri = authorElement.findChildContent("uri");
|
||||
if (uri != null && uri.startsWith("xmpp:")) {
|
||||
try {
|
||||
author = Jid.of(uri.substring(5));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
Date published = null;
|
||||
final String publishedString = entry.findChildContent("published");
|
||||
if (publishedString != null) {
|
||||
try {
|
||||
published = new Date(parseTimestamp(publishedString));
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
published = new Date(parseTimestampAtom(publishedString));
|
||||
} catch (ParseException error) {
|
||||
Log.e("Feeds", "Couldn't parse timestamp " + publishedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String commentsNode = null;
|
||||
String attachmentUrl = null;
|
||||
String attachmentType = null;
|
||||
for (Element child : entry.getChildren()) {
|
||||
if ("link".equals(child.getName()) && Namespace.ATOM.equals(child.getNamespace())) {
|
||||
String rel = child.getAttribute("rel");
|
||||
if ("replies".equals(rel)) {
|
||||
commentsNode = child.getAttribute("href");
|
||||
} else if ("enclosure".equals(rel)) {
|
||||
attachmentUrl = child.getAttribute("href");
|
||||
attachmentType = child.getAttribute("type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Post(id, title, content, author, published, commentsNode, attachmentUrl, attachmentType);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public Jid getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public Date getPublished() {
|
||||
return published;
|
||||
}
|
||||
|
||||
public String getCommentsNode() {
|
||||
return commentsNode;
|
||||
}
|
||||
|
||||
public String getAttachmentUrl() {
|
||||
return attachmentUrl;
|
||||
}
|
||||
|
||||
public String getAttachmentType() {
|
||||
return attachmentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Post post = (Post) o;
|
||||
return Objects.equals(id, post.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
|
||||
public static Post fromCursor(android.database.Cursor cursor) {
|
||||
final String uuid = cursor.getString(cursor.getColumnIndex(UUID));
|
||||
final String authorJidStr = cursor.getString(cursor.getColumnIndex(AUTHOR_JID));
|
||||
eu.siacs.conversations.xmpp.Jid authorJid = null;
|
||||
try {
|
||||
if(authorJidStr != null) {
|
||||
authorJid = eu.siacs.conversations.xmpp.Jid.of(authorJidStr);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
//ignore
|
||||
}
|
||||
final String title = cursor.getString(cursor.getColumnIndex(TITLE));
|
||||
final String content = cursor.getString(cursor.getColumnIndex(CONTENT));
|
||||
final String attachmentUrl = cursor.getString(cursor.getColumnIndex(ATTACHMENT_URL));
|
||||
final String attachmentType = cursor.getString(cursor.getColumnIndex(ATTACHMENT_TYPE));
|
||||
final long published = cursor.getLong(cursor.getColumnIndex(PUBLISHED));
|
||||
final String commentsNode = cursor.getString(cursor.getColumnIndex(COMMENTS_NODE));
|
||||
return new Post(uuid, title, content, authorJid, new java.util.Date(published), commentsNode, attachmentUrl, attachmentType);
|
||||
}
|
||||
|
||||
public android.content.ContentValues getContentValues(eu.siacs.conversations.entities.Account account) {
|
||||
android.content.ContentValues values = new android.content.ContentValues();
|
||||
values.put(UUID, getId());
|
||||
values.put(ACCOUNT_UUID, account.getUuid());
|
||||
values.put(AUTHOR_JID, getAuthor() != null ? getAuthor().toString() : null);
|
||||
values.put(TITLE, getTitle());
|
||||
values.put(CONTENT, getContent());
|
||||
values.put(ATTACHMENT_URL, getAttachmentUrl());
|
||||
values.put(ATTACHMENT_TYPE, getAttachmentType());
|
||||
values.put(PUBLISHED, getPublished() != null ? getPublished().getTime() : 0);
|
||||
values.put(COMMENTS_NODE, getCommentsNode());
|
||||
return values;
|
||||
}
|
||||
}
|
||||
202
src/main/java/eu/siacs/conversations/entities/Story.java
Normal file
202
src/main/java/eu/siacs/conversations/entities/Story.java
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import static eu.siacs.conversations.parser.AbstractParser.parseTimestamp;
|
||||
import static eu.siacs.conversations.parser.AbstractParser.parseTimestampAtom;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class Story extends AbstractEntity {
|
||||
|
||||
public static final String CONTACT = "contact";
|
||||
public static final String URL = "url";
|
||||
public static final String TYPE = "type";
|
||||
public static final String TITLE = "title";
|
||||
public static final String PUBLISHED = "published";
|
||||
|
||||
private final Jid contact;
|
||||
private final String url;
|
||||
private final String type;
|
||||
private final String title;
|
||||
private final long published;
|
||||
|
||||
public Story(final String uuid, final Jid contact, final String url, final String type, final String title, final long published) {
|
||||
this.uuid = uuid;
|
||||
this.contact = contact;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.published = published;
|
||||
}
|
||||
|
||||
public static Story fromCursor(Cursor cursor) {
|
||||
return new Story(
|
||||
cursor.getString(cursor.getColumnIndex(UUID)),
|
||||
Jid.of(cursor.getString(cursor.getColumnIndex(CONTACT))),
|
||||
cursor.getString(cursor.getColumnIndex(URL)),
|
||||
cursor.getString(cursor.getColumnIndex(TYPE)),
|
||||
cursor.getString(cursor.getColumnIndex(TITLE)),
|
||||
cursor.getLong(cursor.getColumnIndex(PUBLISHED))
|
||||
);
|
||||
}
|
||||
|
||||
public static Story fromElement(Element item, Jid contact) {
|
||||
return fromElement(item, contact, 0);
|
||||
}
|
||||
|
||||
public static Story fromElement(Element item, Jid contact, long fallbackTimestamp) {
|
||||
final Element entry = item.findChild("entry", "http://www.w3.org/2005/Atom");
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Element author = entry.findChild("author", "http://www.w3.org/2005/Atom");
|
||||
if (author != null) {
|
||||
final Element uri = author.findChild("uri", "http://www.w3.org/2005/Atom");
|
||||
String authorUri = uri != null ? uri.getContent() : null;
|
||||
if (authorUri != null && authorUri.startsWith("xmpp:")) {
|
||||
try {
|
||||
Jid authorJid = Jid.of(authorUri.substring(5));
|
||||
if (!authorJid.asBareJid().equals(contact.asBareJid())) {
|
||||
android.util.Log.w(eu.siacs.conversations.Config.LOGTAG, "Story author JID (" + authorJid + ") does not match publisher JID (" + contact + "). Ignoring story.");
|
||||
return null;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
android.util.Log.w(eu.siacs.conversations.Config.LOGTAG, "Invalid JID in story author URI: " + authorUri);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Element link = null;
|
||||
final List<Element> children = entry.getChildren();
|
||||
if (children != null) {
|
||||
for (Element child : children) {
|
||||
if ("link".equals(child.getName()) && "enclosure".equals(child.getAttribute("rel"))) {
|
||||
link = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (link == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long timestamp = 0;
|
||||
|
||||
if (fallbackTimestamp > 0) {
|
||||
timestamp = fallbackTimestamp;
|
||||
} else {
|
||||
Element published = entry.findChild("published", "http://www.w3.org/2005/Atom");
|
||||
String publishedContent = published == null ? null : published.getContent();
|
||||
if (publishedContent != null) {
|
||||
try {
|
||||
timestamp = parseTimestamp(publishedContent);
|
||||
} catch (ParseException e) {
|
||||
Log.e("Story", "Couldn't parse timestamp " + publishedContent);
|
||||
}
|
||||
}
|
||||
if (timestamp == 0) {
|
||||
try {
|
||||
timestamp = parseTimestampAtom(publishedContent);
|
||||
} catch (ParseException e) {
|
||||
Log.e("Story", "Couldn't parse timestamp " + publishedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp == 0) {
|
||||
Element updated = entry.findChild("updated", "http://www.w3.org/2005/Atom");
|
||||
String updatedContent = updated == null ? null : updated.getContent();
|
||||
if (updatedContent != null) {
|
||||
try {
|
||||
timestamp = parseTimestamp(updatedContent);
|
||||
} catch (ParseException e) {
|
||||
}
|
||||
}
|
||||
if (timestamp == 0) {
|
||||
try {
|
||||
timestamp = parseTimestampAtom(updatedContent);
|
||||
} catch (ParseException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp == 0) {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
return new Story(
|
||||
item.getAttribute("id"),
|
||||
contact,
|
||||
link.getAttribute("href"),
|
||||
link.getAttribute("type"),
|
||||
entry.findChildContent("title", "http://www.w3.org/2005/Atom"),
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
|
||||
public static List<Story> parseFromPubSub(Element pubsub, Jid contact) {
|
||||
List<Story> stories = new ArrayList<>();
|
||||
if (pubsub == null) {
|
||||
return stories;
|
||||
}
|
||||
Element items = pubsub.findChild("items");
|
||||
if (items != null) {
|
||||
final List<Element> children = items.getChildren();
|
||||
if (children != null) {
|
||||
for (Element item : children) {
|
||||
if (item.getName().equals("item")) {
|
||||
long timestamp = parseTimestamp(item, 0L);
|
||||
Story story = fromElement(item, contact, timestamp);
|
||||
if (story != null) {
|
||||
stories.add(story);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return stories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(UUID, uuid);
|
||||
values.put(CONTACT, contact.toString());
|
||||
values.put(URL, url);
|
||||
values.put(TYPE, type);
|
||||
values.put(TITLE, title);
|
||||
values.put(PUBLISHED, published);
|
||||
return values;
|
||||
}
|
||||
|
||||
public Jid getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public long getPublished() {
|
||||
return published;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ public abstract class AbstractGenerator {
|
|||
Namespace.OOB,
|
||||
"http://jabber.org/protocol/caps",
|
||||
"http://jabber.org/protocol/disco#info",
|
||||
Namespace.PUBSUB,
|
||||
"urn:xmpp:avatar:metadata+notify",
|
||||
Namespace.NICK + "+notify",
|
||||
"urn:xmpp:ping",
|
||||
|
|
@ -41,6 +42,12 @@ public abstract class AbstractGenerator {
|
|||
"http://jabber.org/protocol/chatstates",
|
||||
Namespace.REACTIONS,
|
||||
Namespace.USER_TUNE + "+notify",
|
||||
Namespace.PUBSUB_SOCIAL_FEED,
|
||||
Namespace.PUBSUB_SOCIAL_FEED + "+notify",
|
||||
Namespace.MICROBLOG,
|
||||
Namespace.MICROBLOG + "+notify",
|
||||
Namespace.PUBSUB_STORIES,
|
||||
Namespace.PUBSUB_STORIES + "+notify",
|
||||
};
|
||||
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
||||
"urn:xmpp:chat-markers:0", "urn:xmpp:receipts"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import java.io.IOException;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
|
@ -244,6 +246,47 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return publish(namespace, item, options);
|
||||
}
|
||||
|
||||
public Iq publishStory(final Account account, final String url, final String type, final String title, Bundle options) {
|
||||
final Element item = new Element("item");
|
||||
final String storyId = UUID.randomUUID().toString();
|
||||
item.setAttribute("id", storyId);
|
||||
final Element entry = item.addChild("entry", Namespace.ATOM);
|
||||
|
||||
entry.addChild("id").setContent("urn:uuid:" + storyId);
|
||||
|
||||
String effectiveTitle = title;
|
||||
if (Strings.isNullOrEmpty(effectiveTitle)) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
|
||||
effectiveTitle = "Story " + sdf.format(new Date());
|
||||
}
|
||||
entry.addChild("title").setContent(effectiveTitle);
|
||||
|
||||
final String timestamp = getTimestamp(System.currentTimeMillis());
|
||||
entry.addChild("updated").setContent(timestamp);
|
||||
entry.addChild("published").setContent(timestamp);
|
||||
|
||||
if (account != null) {
|
||||
entry.addChild("author").addChild("uri").setContent("xmpp:" + account.getJid().asBareJid());
|
||||
}
|
||||
|
||||
final Element link = entry.addChild("link");
|
||||
link.setAttribute("rel", "enclosure");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("type", type);
|
||||
link.setAttribute("title", effectiveTitle);
|
||||
|
||||
return publish(Namespace.PUBSUB_STORIES, item, options);
|
||||
}
|
||||
|
||||
public Iq retrieveStories(Jid jid) {
|
||||
final Iq iq = new Iq(Iq.Type.GET);
|
||||
iq.setTo(jid);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element items = pubsub.addChild("items");
|
||||
items.setAttribute("node", Namespace.PUBSUB_STORIES);
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) {
|
||||
final Element item = new Element("item");
|
||||
item.setAttribute("id", avatar.sha1sum);
|
||||
|
|
@ -672,6 +715,31 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return options;
|
||||
}
|
||||
|
||||
public static Bundle defaultStoriesConfiguration() {
|
||||
Bundle options = new Bundle();
|
||||
options.putString("pubsub#node_type", "leaf");
|
||||
options.putString("pubsub#type", Namespace.PUBSUB_STORIES);
|
||||
options.putString("pubsub#access_model", "roster");
|
||||
options.putString("pubsub#item_expire", "86400");
|
||||
options.putString("pubsub#persist_items", "1");
|
||||
options.putString("pubsub#max_items", "120");
|
||||
options.putString("pubsub#notify_retract", "1");
|
||||
options.putString("pubsub#send_last_published_item", "on_sub_and_presence");
|
||||
options.putString("pubsub#publish_model", "publishers");
|
||||
return options;
|
||||
}
|
||||
|
||||
public Iq createStoriesNode() {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
pubsub.addChild("create").setAttribute("node", Namespace.PUBSUB_STORIES);
|
||||
final Element configure = pubsub.addChild("configure");
|
||||
// Correctly use the 'pubsub#node_config' namespace
|
||||
final Data data = Data.create("http://jabber.org/protocol/pubsub#node_config", defaultStoriesConfiguration());
|
||||
configure.addChild(data);
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq requestPubsubConfiguration(Jid jid, String node) {
|
||||
return pubsubConfiguration(jid, node, null);
|
||||
}
|
||||
|
|
@ -746,4 +814,164 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public Iq retrievePubsubItems(final Jid server, final String node) {
|
||||
final Iq iq = new Iq(Iq.Type.GET);
|
||||
if (server != null) {
|
||||
iq.setTo(server);
|
||||
}
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element items = pubsub.addChild("items");
|
||||
items.setAttribute("node", node);
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq publishPost(final Account account, final String title, final String content, final String attachmentUrl, final String attachmentType, final String postId) {
|
||||
final Element item = new Element("item");
|
||||
item.setAttribute("id", postId);
|
||||
final Element entry = item.addChild("entry", Namespace.ATOM);
|
||||
|
||||
entry.addChild("id").setContent("urn:uuid:" + postId);
|
||||
|
||||
entry.addChild("link")
|
||||
.setAttribute("rel", "replies")
|
||||
.setAttribute("title", "comments")
|
||||
.setAttribute("href", "xmpp:" + account.getJid().asBareJid() + "?;node=urn:xmpp:microblog:0:comments/" + postId);
|
||||
|
||||
if (title != null) {
|
||||
entry.addChild("title").setContent(title);
|
||||
}
|
||||
if (content != null) {
|
||||
entry.addChild("content").setContent(content);
|
||||
}
|
||||
if (attachmentUrl != null && attachmentType != null) {
|
||||
entry.addChild("link")
|
||||
.setAttribute("rel", "enclosure")
|
||||
.setAttribute("href", attachmentUrl)
|
||||
.setAttribute("type", attachmentType);
|
||||
}
|
||||
final Element author = entry.addChild("author");
|
||||
String name = account.getDisplayName();
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
name = account.getJid().asBareJid().toString();
|
||||
}
|
||||
author.addChild("name").setContent(name);
|
||||
author.addChild("uri").setContent("xmpp:" + account.getJid().asBareJid().toString());
|
||||
final String now = AbstractGenerator.getTimestamp(System.currentTimeMillis());
|
||||
entry.addChild("published").setContent(now);
|
||||
entry.addChild("updated").setContent(now);
|
||||
|
||||
return publish(Namespace.MICROBLOG, item, null);
|
||||
}
|
||||
|
||||
|
||||
public static Bundle defaultPostConfiguration() {
|
||||
Bundle options = new Bundle();
|
||||
options.putString("pubsub#node_type", "leaf");
|
||||
options.putString("pubsub#type", Namespace.PUBSUB_SOCIAL_FEED);
|
||||
options.putString("pubsub#access_model", "roster");
|
||||
options.putString("pubsub#persist_items", "1");
|
||||
options.putString("pubsub#deliver_payloads", "1");
|
||||
options.putString("pubsub#send_last_published_item", "on_sub");
|
||||
options.putString("pubsub#max_items", "max");
|
||||
options.putString("pubsub#notify_retract", "1");
|
||||
options.putString("pubsub#deliver_notifications", "1");
|
||||
options.putString("pubsub#publish_model", "publishers");
|
||||
return options;
|
||||
}
|
||||
|
||||
public static Bundle defaultCommentsConfiguration() {
|
||||
Bundle options = new Bundle();
|
||||
options.putString("pubsub#node_type", "leaf");
|
||||
options.putString("pubsub#type", "urn:xmpp:microblog:0:comments");
|
||||
options.putString("pubsub#access_model", "open");
|
||||
options.putString("pubsub#persist_items", "1");
|
||||
options.putString("pubsub#max_items", "max");
|
||||
options.putString("pubsub#notify_retract", "1");
|
||||
options.putString("pubsub#deliver_notifications", "1");
|
||||
options.putString("pubsub#deliver_payloads", "1");
|
||||
options.putString("pubsub#send_last_published_item", "on_sub");
|
||||
options.putString("pubsub#publish_model", "open");
|
||||
options.putString("pubsub#itemreply", "publisher");
|
||||
return options;
|
||||
}
|
||||
|
||||
private Iq createNode(String node, Bundle options) {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
pubsub.addChild("create").setAttribute("node", node);
|
||||
final Element configure = pubsub.addChild("configure");
|
||||
final Data data = Data.create("http://jabber.org/protocol/pubsub#node_config", options);
|
||||
configure.addChild(data);
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq createSocialFeedNode() {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
pubsub.addChild("create").setAttribute("node", Namespace.MICROBLOG);
|
||||
final Element configure = pubsub.addChild("configure");
|
||||
final Data data = Data.create("http://jabber.org/protocol/pubsub#node_config", defaultPostConfiguration());
|
||||
configure.addChild(data);
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq publishComment(final Account account, final String node, final String title) {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
iq.setTo(account.getJid().asBareJid());
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element publish = pubsub.addChild("publish");
|
||||
publish.setAttribute("node", node);
|
||||
final Element item = publish.addChild("item");
|
||||
final Element entry = item.addChild("entry", Namespace.ATOM);
|
||||
entry.addChild("title").setContent(title);
|
||||
final Element author = entry.addChild("author");
|
||||
String name = account.getDisplayName();
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
name = account.getJid().asBareJid().toString();
|
||||
}
|
||||
author.addChild("name").setContent(name);
|
||||
author.addChild("uri").setContent("xmpp:" + account.getJid().asBareJid().toString());
|
||||
final String id = "tag:" + account.getServer() + "," + AbstractGenerator.getTimestamp(System.currentTimeMillis()) + ":" + UUID.randomUUID().toString();
|
||||
entry.addChild("id").setContent(id);
|
||||
final String now = AbstractGenerator.getTimestamp(System.currentTimeMillis());
|
||||
entry.addChild("published").setContent(now);
|
||||
entry.addChild("updated").setContent(now);
|
||||
return iq;
|
||||
}
|
||||
|
||||
|
||||
public Iq createCommentsNode(String postId) {
|
||||
return createNode("urn:xmpp:microblog:0:comments/" + postId, defaultCommentsConfiguration());
|
||||
}
|
||||
|
||||
public Iq retractPost(final String node, final String id) {
|
||||
final var packet = new Iq(Iq.Type.SET);
|
||||
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element retract = pubsub.addChild("retract");
|
||||
retract.setAttribute("node", node);
|
||||
retract.setAttribute("notify", "true");
|
||||
retract.addChild("item").setAttribute("id", id);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public Iq generateSubscriptionIq(final Jid to, final String node, final Jid from) {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
iq.setTo(to);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element subscribe = pubsub.addChild("subscribe");
|
||||
subscribe.setAttribute("node", node);
|
||||
subscribe.setAttribute("jid", from.asBareJid().toString());
|
||||
return iq;
|
||||
}
|
||||
|
||||
public Iq generateUnsubscriptionIq(final Jid to, final String node, final Jid from) {
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
iq.setTo(to);
|
||||
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element unsubscribe = pubsub.addChild("unsubscribe");
|
||||
unsubscribe.setAttribute("node", node);
|
||||
unsubscribe.setAttribute("jid", from.asBareJid().toString());
|
||||
return iq;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import eu.siacs.conversations.entities.DownloadableFile;
|
|||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
|
@ -121,7 +122,15 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
HttpUploadConnection connection = new HttpUploadConnection(message, Method.determine(message.getConversation().getAccount()), this, cb);
|
||||
connection.init(delay);
|
||||
connection.initForMessage(delay);
|
||||
this.uploadConnections.add(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public void createNewUploadConnection(final DownloadableFile file, final Account account, final boolean delay, final UiCallback<String> callback) {
|
||||
synchronized (this.uploadConnections) {
|
||||
HttpUploadConnection connection = new HttpUploadConnection(account, file, Method.determine(account), this, callback);
|
||||
connection.initForFile();
|
||||
this.uploadConnections.add(connection);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ import java.util.List;
|
|||
import java.util.concurrent.Future;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
|
|
@ -45,6 +47,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
private final Method method;
|
||||
private boolean delayed = false;
|
||||
private DownloadableFile file;
|
||||
@Nullable
|
||||
private final Message message;
|
||||
private SlotRequester.Slot slot;
|
||||
private byte[] key = null;
|
||||
|
|
@ -52,14 +55,34 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
private long transmitted = 0;
|
||||
private Call mostRecentCall;
|
||||
private ListenableFuture<SlotRequester.Slot> slotFuture;
|
||||
private Runnable cb;
|
||||
|
||||
@Nullable
|
||||
private final Runnable cb;
|
||||
|
||||
@Nullable
|
||||
private final UiCallback<String> callback;
|
||||
@NonNull
|
||||
private final Account account;
|
||||
|
||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager, Runnable cb) {
|
||||
this.message = message;
|
||||
this.account = message.getConversation().getAccount();
|
||||
this.method = method;
|
||||
this.mHttpConnectionManager = httpConnectionManager;
|
||||
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
|
||||
this.cb = cb;
|
||||
this.callback = null;
|
||||
}
|
||||
|
||||
public HttpUploadConnection(Account account, DownloadableFile file, Method method, HttpConnectionManager httpConnectionManager, UiCallback<String> callback) {
|
||||
this.message = null;
|
||||
this.account = account;
|
||||
this.file = file;
|
||||
this.method = method;
|
||||
this.mHttpConnectionManager = httpConnectionManager;
|
||||
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
|
||||
this.callback = callback;
|
||||
this.cb = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -90,13 +113,13 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
|
||||
if (slotFuture != null && !slotFuture.isDone()) {
|
||||
if (slotFuture.cancel(true)) {
|
||||
Log.d(Config.LOGTAG,"cancelled slot requester");
|
||||
Log.d(Config.LOGTAG, "cancelled slot requester");
|
||||
}
|
||||
}
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
Log.d(Config.LOGTAG,"cancelled HTTP request");
|
||||
Log.d(Config.LOGTAG, "cancelled HTTP request");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,17 +128,22 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
final Call call = this.mostRecentCall;
|
||||
final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
|
||||
final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled());
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
||||
if (cb != null) cb.run();
|
||||
if (this.message != null) {
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
||||
if (cb != null) cb.run();
|
||||
} else if (this.callback != null) {
|
||||
callback.error(R.string.upload_failed_server_not_found, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
mHttpConnectionManager.finishUploadConnection(this);
|
||||
message.setTransferable(null);
|
||||
if (this.message != null) {
|
||||
message.setTransferable(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void init(boolean delay) {
|
||||
final Account account = message.getConversation().getAccount();
|
||||
public void initForMessage(boolean delay) {
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
final String mime;
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||
|
|
@ -147,7 +175,6 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
@Override
|
||||
public void onFailure(@NonNull final Throwable throwable) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
|
||||
// TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence
|
||||
fail(throwable.getMessage());
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
|
|
@ -155,10 +182,40 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
|
||||
}
|
||||
|
||||
public void initForFile() {
|
||||
final String mime = this.file.getMimeType();
|
||||
final long originalFileSize = file.getSize();
|
||||
this.delayed = false;
|
||||
if (Config.ENCRYPT_ON_HTTP_UPLOADED) {
|
||||
this.key = new byte[44];
|
||||
SECURE_RANDOM.nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||
this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, file.getName(), mime);
|
||||
Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable SlotRequester.Slot result) {
|
||||
HttpUploadConnection.this.slot = result;
|
||||
try {
|
||||
HttpUploadConnection.this.upload();
|
||||
} catch (final Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull final Throwable throwable) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
|
||||
fail(throwable.getMessage());
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void upload() {
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
slot.put,
|
||||
message.getConversation().getAccount(),
|
||||
account,
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
|
@ -178,7 +235,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
||||
final int code = response.code();
|
||||
if (code == 200 || code == 201) {
|
||||
Log.d(Config.LOGTAG, "finished uploading file");
|
||||
|
|
@ -188,13 +245,19 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
} else {
|
||||
get = slot.get.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
finish();
|
||||
if (!message.isPrivateMessage()) {
|
||||
message.setCounterpart(message.getConversation().getJid().asBareJid());
|
||||
if (message != null) {
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
finish();
|
||||
if (!message.isPrivateMessage()) {
|
||||
message.setCounterpart(message.getConversation().getJid().asBareJid());
|
||||
}
|
||||
mXmppConnectionService.resendMessage(message, delayed, cb);
|
||||
} else if (callback != null) {
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
finish();
|
||||
callback.success(get);
|
||||
}
|
||||
mXmppConnectionService.resendMessage(message, delayed, cb);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
|
||||
fail("http upload failed because response code was " + code);
|
||||
|
|
@ -212,4 +275,4 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
|||
this.transmitted = progress;
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,26 @@ public abstract class AbstractParser {
|
|||
return Math.min(dateFormat.parse(timestamp).getTime() + ms, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static long parseTimestampAtom(String timestamp) throws ParseException {
|
||||
timestamp = timestamp.replace("+00:00", "+0000");
|
||||
SimpleDateFormat dateFormat;
|
||||
long ms;
|
||||
if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') {
|
||||
String millis = timestamp.substring(19, timestamp.length() - 5);
|
||||
try {
|
||||
double fractions = Double.parseDouble("0" + millis);
|
||||
ms = Math.round(1000 * fractions);
|
||||
} catch (NumberFormatException e) {
|
||||
ms = 0;
|
||||
}
|
||||
} else {
|
||||
ms = 0;
|
||||
}
|
||||
timestamp = timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5);
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
|
||||
return Math.min(dateFormat.parse(timestamp).getTime() + ms, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static long getTimestamp(final String input) throws ParseException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("timestamp should not be null");
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import com.google.common.io.BaseEncoding;
|
|||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Comment;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.entities.Room;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
|
@ -438,6 +441,57 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
|
|||
account.getRoster().markAllAsNotInRoster();
|
||||
}
|
||||
this.rosterItems(account, query);
|
||||
} else if (packet.hasChild("pubsub", Namespace.PUBSUB)) {
|
||||
Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
|
||||
Element items = pubsub.findChild("items");
|
||||
if (items != null) {
|
||||
String node = items.getAttribute("node");
|
||||
if (Namespace.PUBSUB_STORIES.equals(node)) {
|
||||
Jid from = packet.getFrom();
|
||||
if (from != null) {
|
||||
for (Element item : items.getChildren()) {
|
||||
if (item.getName().equals("item")) {
|
||||
Story story = Story.fromElement(item, from);
|
||||
if (story != null) {
|
||||
mXmppConnectionService.onStoryReceived(story);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (node != null && node.equals(Namespace.ATOM) || node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
|
||||
for (Element child : items.getChildren()) {
|
||||
if ("item".equals(child.getName())) {
|
||||
final String postId = child.getAttribute("id");
|
||||
Element entry = child.findChild("entry", Namespace.ATOM);
|
||||
if (entry != null) {
|
||||
try {
|
||||
Element inReplyTo = entry.findChild("in-reply-to", "http://purl.org/syndication/thread/1.0");
|
||||
if (inReplyTo != null) {
|
||||
Comment comment = Comment.fromElement(entry);
|
||||
String originalPostUuid = inReplyTo.getAttribute("ref");
|
||||
if (originalPostUuid != null && originalPostUuid.startsWith("urn:uuid:")) {
|
||||
originalPostUuid = originalPostUuid.substring(9);
|
||||
}
|
||||
mXmppConnectionService.notifyOnCommentReceived(originalPostUuid, comment);
|
||||
} else {
|
||||
Post post = Post.fromElement(entry);
|
||||
mXmppConnectionService.onPostReceived(post, account);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "error creating post/comment from pubsub item in iq", e);
|
||||
}
|
||||
} else if (postId != null) {
|
||||
mXmppConnectionService.onPostRetracted(postId);
|
||||
}
|
||||
} else if ("retract".equals(child.getName())) {
|
||||
final String postId = child.getAttribute("id");
|
||||
if (postId != null) {
|
||||
mXmppConnectionService.onPostRetracted(postId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ((packet.hasChild("block", Namespace.BLOCKING)
|
||||
|| packet.hasChild("blocklist", Namespace.BLOCKING))
|
||||
&& packet.fromServer(account)) {
|
||||
|
|
|
|||
|
|
@ -35,8 +35,11 @@ import java.util.function.Consumer;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import eu.siacs.conversations.crypto.OtrService;
|
||||
import eu.siacs.conversations.entities.Comment;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.xmpp.pep.UserTune;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
|
|
@ -464,6 +467,26 @@ public class MessageParser extends AbstractParser
|
|||
&& account.getJid().asBareJid().equals(from)) {
|
||||
final Element item = items.findChild("item");
|
||||
mXmppConnectionService.processMdsItem(account, item);
|
||||
} else if (Namespace.PUBSUB_STORIES.equals(node)) {
|
||||
final Element retract = items.findChild("retract");
|
||||
if (retract != null) {
|
||||
final String id = retract.getAttribute("id");
|
||||
if (id != null) {
|
||||
mXmppConnectionService.onStoryRetracted(id);
|
||||
}
|
||||
} else {
|
||||
final List<Element> children = items.getChildren();
|
||||
if (children != null) {
|
||||
for (Element item : children) {
|
||||
if ("item".equals(item.getName())) {
|
||||
final Story story = Story.fromElement(item, from);
|
||||
if (story != null) {
|
||||
mXmppConnectionService.onStoryReceived(story);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (Namespace.USER_TUNE.equals(node)) {
|
||||
final Conversation conversation =
|
||||
mXmppConnectionService.find(account, from.asBareJid());
|
||||
|
|
@ -1732,11 +1755,41 @@ public class MessageParser extends AbstractParser
|
|||
packet);
|
||||
}
|
||||
|
||||
final Element event =
|
||||
original.findChild("event", "http://jabber.org/protocol/pubsub#event");
|
||||
if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
|
||||
if (event.hasChild("items")) {
|
||||
parseEvent(event, original.getFrom(), account);
|
||||
final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
|
||||
if (event != null) {
|
||||
final Element items = event.findChild("items");
|
||||
if (items != null) {
|
||||
final String node = items.getAttribute("node");
|
||||
if (node != null && node.equals(Namespace.ATOM) || node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
|
||||
for (Element child : items.getChildren()) {
|
||||
if ("item".equals(child.getName())) {
|
||||
Element entry = child.findChild("entry", Namespace.ATOM);
|
||||
if (entry != null) {
|
||||
try {
|
||||
Element inReplyTo = entry.findChild("in-reply-to", "http://purl.org/syndication/thread/1.0");
|
||||
if (inReplyTo != null) {
|
||||
Comment comment = Comment.fromElement(entry);
|
||||
String originalPostUuid = inReplyTo.getAttribute("ref");
|
||||
if (originalPostUuid != null && originalPostUuid.startsWith("urn:uuid:")) {
|
||||
originalPostUuid = originalPostUuid.substring(9);
|
||||
}
|
||||
mXmppConnectionService.notifyOnCommentReceived(originalPostUuid, comment);
|
||||
} else {
|
||||
Post post = Post.fromElement(entry);
|
||||
mXmppConnectionService.onPostReceived(post, account);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "error creating post/comment from pubsub item in message", e);
|
||||
}
|
||||
}
|
||||
} else if ("retract".equals(child.getName())) {
|
||||
final String postId = child.getAttribute("id");
|
||||
mXmppConnectionService.onPostRetracted(postId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parseEvent(event, original.getFrom(), account);
|
||||
}
|
||||
} else if (event.hasChild("delete")) {
|
||||
parseDeleteEvent(event, original.getFrom(), account);
|
||||
} else if (event.hasChild("purge")) {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
|||
public class DatabaseBackend extends SQLiteOpenHelper {
|
||||
|
||||
private static final String DATABASE_NAME = "history";
|
||||
private static final int DATABASE_VERSION = 64;
|
||||
private static final int DATABASE_VERSION = 66;
|
||||
|
||||
private static boolean requiresMessageIndexRebuild = false;
|
||||
private static DatabaseBackend instance = null;
|
||||
|
|
@ -139,6 +139,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ " TEXT, "
|
||||
+ Contact.LAST_PRESENCE
|
||||
+ " TEXT, "
|
||||
+ Contact.CALLS_DISABLED +
|
||||
" boolean DEFAULT 0,"
|
||||
+ Contact.LAST_TIME
|
||||
+ " NUMBER, "
|
||||
+ Contact.RTP_CAPABILITY
|
||||
|
|
@ -374,6 +376,21 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
private static final String COPY_PREEXISTING_ENTRIES =
|
||||
"INSERT INTO messages_index(messages_index) VALUES('rebuild');";
|
||||
|
||||
private static final String CREATE_POSTS_TABLE =
|
||||
"CREATE TABLE " + eu.siacs.conversations.entities.Post.TABLENAME + " ("
|
||||
+ eu.siacs.conversations.entities.Post.UUID + " TEXT PRIMARY KEY,"
|
||||
+ eu.siacs.conversations.entities.Post.ACCOUNT_UUID + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.AUTHOR_JID + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.TITLE + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.CONTENT + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.ATTACHMENT_URL + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.ATTACHMENT_TYPE + " TEXT,"
|
||||
+ eu.siacs.conversations.entities.Post.PUBLISHED + " NUMBER,"
|
||||
+ eu.siacs.conversations.entities.Post.COMMENTS_NODE + " TEXT,"
|
||||
+ "FOREIGN KEY(" + eu.siacs.conversations.entities.Post.ACCOUNT_UUID + ") REFERENCES "
|
||||
+ eu.siacs.conversations.entities.Account.TABLENAME
|
||||
+ "(" + eu.siacs.conversations.entities.Account.UUID + ") ON DELETE CASCADE);";
|
||||
|
||||
protected Context context;
|
||||
|
||||
private DatabaseBackend(Context context) {
|
||||
|
|
@ -454,7 +471,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ " TEXT,"
|
||||
+ Account.FAST_TOKEN
|
||||
+ " TEXT,"
|
||||
+ "ordering INTEGER DEFAULT 0,"
|
||||
+ Account.ORDERING
|
||||
+ " INTEGER DEFAULT 0,"
|
||||
+ Account.PORT
|
||||
+ " NUMBER DEFAULT 5222)");
|
||||
db.execSQL(
|
||||
|
|
@ -565,6 +583,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
|
||||
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
|
||||
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
|
||||
db.execSQL(CREATE_POSTS_TABLE);
|
||||
monoclesDatabase(db);
|
||||
}
|
||||
|
||||
|
|
@ -1298,11 +1317,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
if (oldVersion < 64 && newVersion >= 64) {
|
||||
try {
|
||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN ordering INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.ORDERING + " INTEGER DEFAULT 0");
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "Failed to add ordering column to account table", e);
|
||||
}
|
||||
}
|
||||
if (oldVersion < 65 && newVersion >= 65) {
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.CALLS_DISABLED + " boolean DEFAULT 0");
|
||||
}
|
||||
if (oldVersion < 66 && newVersion >= 66) {
|
||||
db.execSQL(CREATE_POSTS_TABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1607,7 +1632,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
public void createAccount(Account account) {
|
||||
final var db = this.getWritableDatabase();
|
||||
final ContentValues values = account.getContentValues();
|
||||
values.put("ordering", account.getOrdering());
|
||||
db.insert(Account.TABLENAME, null, values);
|
||||
}
|
||||
|
||||
|
|
@ -2200,6 +2224,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
return message;
|
||||
}
|
||||
|
||||
public boolean updateContact(final Contact contact) {
|
||||
final SQLiteDatabase db = this.getWritableDatabase();final ContentValues values = contact.getContentValues();
|
||||
final String[] args = {contact.getAccount().getUuid(), contact.getJid().asBareJid().toString()};
|
||||
try {
|
||||
final int count = db.update(Contact.TABLENAME, values,
|
||||
Contact.ACCOUNT + "=? AND " + Contact.JID + "=?",
|
||||
args);
|
||||
return count > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FilePath {
|
||||
public final UUID uuid;
|
||||
public final String path;
|
||||
|
|
@ -2339,13 +2376,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
private List<Account> getAccounts(SQLiteDatabase db) {
|
||||
final List<Account> list = new ArrayList<>();
|
||||
try (final Cursor cursor =
|
||||
db.query(Account.TABLENAME, null, null, null, null, null, "ordering ASC")) {
|
||||
db.query(Account.TABLENAME, null, null, null, null, null, Account.ORDERING + " ASC")) { // Use constant here
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
list.add(Account.fromCursor(cursor));
|
||||
}
|
||||
}
|
||||
// Ensure you read the value if needed, though typically we just rely on the sort order
|
||||
// account.setOrder(cursor.getInt(cursor.getColumnIndex("ordering")));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
@ -2353,7 +2388,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
final var db = this.getWritableDatabase();
|
||||
final String[] args = {account.getUuid()};
|
||||
final ContentValues values = account.getContentValues();
|
||||
values.put("ordering", account.getOrdering());
|
||||
final int rows =
|
||||
db.update(Account.TABLENAME, values, Account.UUID + "=?", args);
|
||||
return rows == 1;
|
||||
|
|
@ -3354,4 +3388,78 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
// New helper method that accepts an existing database connection
|
||||
public Conversation findConversationByUuid(final String uuid, final SQLiteDatabase db) {
|
||||
final String[] selectionArgs = {uuid};
|
||||
try (final Cursor cursor =
|
||||
db.query(
|
||||
Conversation.TABLENAME,
|
||||
null,
|
||||
Conversation.UUID + "=?",
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
null)) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return null;
|
||||
}
|
||||
final Conversation conversation = Conversation.fromCursor(cursor);
|
||||
if (conversation.getJid() instanceof Jid.Invalid) {
|
||||
return null;
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<Message> getMessages(Conversation conversation, int type, int limit) {
|
||||
ArrayList<Message> list = new ArrayList<>();
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor = db.query(Message.TABLENAME,
|
||||
null,
|
||||
Message.CONVERSATION + "=? AND " + Message.TYPE + "=?",
|
||||
new String[]{conversation.getUuid(), String.valueOf(type)},
|
||||
null,
|
||||
null,
|
||||
Message.TIME_SENT + " DESC",
|
||||
String.valueOf(limit));
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
do {
|
||||
try {
|
||||
list.add(Message.fromCursor(cursor, conversation));
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG,"unable to load message from database",e);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
cursor.close();
|
||||
return list;
|
||||
}
|
||||
|
||||
public void createPost(eu.siacs.conversations.entities.Post post, eu.siacs.conversations.entities.Account account) {
|
||||
final android.database.sqlite.SQLiteDatabase db = this.getWritableDatabase();
|
||||
db.insertWithOnConflict(eu.siacs.conversations.entities.Post.TABLENAME, null, post.getContentValues(account), android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public java.util.List<eu.siacs.conversations.entities.Post> getPosts() {
|
||||
final java.util.List<eu.siacs.conversations.entities.Post> list = new java.util.ArrayList<>();
|
||||
final android.database.sqlite.SQLiteDatabase db = this.getReadableDatabase();
|
||||
android.database.Cursor cursor = db.query(eu.siacs.conversations.entities.Post.TABLENAME, null, null, null, null, null, eu.siacs.conversations.entities.Post.PUBLISHED + " DESC");
|
||||
while (cursor.moveToNext()) {
|
||||
list.add(eu.siacs.conversations.entities.Post.fromCursor(cursor));
|
||||
}
|
||||
cursor.close();
|
||||
return list;
|
||||
}
|
||||
|
||||
public void deletePost(String uuid) {
|
||||
final android.database.sqlite.SQLiteDatabase db = this.getWritableDatabase();
|
||||
db.delete(eu.siacs.conversations.entities.Post.TABLENAME, eu.siacs.conversations.entities.Post.UUID + "=?", new String[]{uuid});
|
||||
}
|
||||
|
||||
public void clearPosts() {
|
||||
final android.database.sqlite.SQLiteDatabase db = this.getWritableDatabase();
|
||||
db.delete(eu.siacs.conversations.entities.Post.TABLENAME, null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2255,6 +2255,13 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public DownloadableFile getTemporaryFile(String mimeType) {
|
||||
final String extension = MimeUtils.guessExtensionFromMimeType(mimeType);
|
||||
final String filename = UUID.randomUUID().toString() + (extension == null ? "" : "." + extension);
|
||||
final File file = new File(mXmppConnectionService.getCacheDir(), filename);
|
||||
return new DownloadableFile(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private int getMediaRuntime(final File file) {
|
||||
try {
|
||||
final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
|
||||
|
|
@ -2428,6 +2435,46 @@ public class FileBackend {
|
|||
return file.delete();
|
||||
}
|
||||
|
||||
public File getStoryCacheDirectory() {
|
||||
File cacheDir = mXmppConnectionService.getCacheDir();
|
||||
File storyCache = new File(cacheDir, "stories");
|
||||
if (!storyCache.exists()) {
|
||||
storyCache.mkdirs();
|
||||
}
|
||||
return storyCache;
|
||||
}
|
||||
|
||||
public File getStoryCacheFile(String url) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update(url.getBytes());
|
||||
byte[] messageDigest = digest.digest();
|
||||
String sha1 = CryptoHelper.bytesToHex(messageDigest);
|
||||
return new File(getStoryCacheDirectory(), sha1);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// This should not happen, but as a fallback, use a random name
|
||||
return new File(getStoryCacheDirectory(), UUID.randomUUID().toString());
|
||||
}
|
||||
}
|
||||
|
||||
public Uri getTakeVideoUri() {
|
||||
final String filename =
|
||||
String.format("IMG_%s.%s", IMAGE_DATE_FORMAT.format(new Date()), "mp4");
|
||||
final File directory;
|
||||
if (Config.ONLY_INTERNAL_STORAGE) {
|
||||
directory = new File(mXmppConnectionService.getCacheDir(), "Camera");
|
||||
} else {
|
||||
directory =
|
||||
new File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DCIM),
|
||||
"Camera");
|
||||
}
|
||||
final File file = new File(directory, filename);
|
||||
file.getParentFile().mkdirs();
|
||||
return getUriForFile(mXmppConnectionService, file, filename);
|
||||
}
|
||||
|
||||
private static class Dimensions {
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
|
|
|||
|
|
@ -2468,4 +2468,10 @@ public class NotificationService {
|
|||
return lastTime;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasNewMissedCalls() {
|
||||
synchronized (mMissedCalls) {
|
||||
return !mMissedCalls.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ import com.kedia.ogparser.JsoupProxy;
|
|||
import com.kedia.ogparser.OpenGraphCallback;
|
||||
import com.kedia.ogparser.OpenGraphParser;
|
||||
import com.kedia.ogparser.OpenGraphResult;
|
||||
import com.otaliastudios.transcoder.Transcoder;
|
||||
import com.otaliastudios.transcoder.TranscoderListener;
|
||||
|
||||
import net.java.otr4j.session.Session;
|
||||
import net.java.otr4j.session.SessionID;
|
||||
|
|
@ -106,11 +108,13 @@ import java.util.List;
|
|||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
|
@ -122,6 +126,11 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import eu.siacs.conversations.Conversations;
|
||||
import eu.siacs.conversations.entities.Comment;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.entities.StubConversation;
|
||||
import eu.siacs.conversations.utils.TranscoderStrategies;
|
||||
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
|
|
@ -290,6 +299,9 @@ public class XmppConnectionService extends Service {
|
|||
private final ScheduledExecutorService userTuneUpdateExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
private ScheduledFuture<?> pendingUserTuneUpdate;
|
||||
|
||||
private final ScheduledExecutorService storyRetractionExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService storyCacheExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
|
||||
new SerialSingleThreadExecutor("VideoCompression");
|
||||
private final SerialSingleThreadExecutor mDatabaseWriterExecutor =
|
||||
|
|
@ -344,19 +356,20 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
if (online) {
|
||||
conversation.endOtrIfNeeded();
|
||||
if (contact.getPresences().size() == 1) {
|
||||
sendUnsentMessages(conversation);
|
||||
}
|
||||
} else {
|
||||
//check if the resource we are haveing a conversation with is still online
|
||||
if (conversation.hasValidOtrSession()) {
|
||||
String otrResource = conversation.getOtrSession().getSessionID().getUserID();
|
||||
if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
|
||||
conversation.endOtrIfNeeded();
|
||||
if (contact.getPresences().size() == 1) {
|
||||
sendUnsentMessages(conversation);
|
||||
}
|
||||
fetchStories(conversation.getAccount(), conversation.getContact());
|
||||
} else {
|
||||
//check if the resource we are haveing a conversation with is still online
|
||||
if (conversation.hasValidOtrSession()) {
|
||||
String otrResource = conversation.getOtrSession().getSessionID().getUserID();
|
||||
if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
|
||||
conversation.endOtrIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
|
||||
private List<Account> accounts;
|
||||
private final JingleConnectionManager mJingleConnectionManager =
|
||||
|
|
@ -791,7 +804,7 @@ public class XmppConnectionService extends Service {
|
|||
message.setEncryption(conversation.getNextEncryption());
|
||||
}
|
||||
if (conversation.getCaption() != null) {
|
||||
message.appendBody(conversation.getCaption().getBody() + " ");
|
||||
message.appendBody(conversation.getCaption() + " ");
|
||||
message.setEncryption(conversation.getNextEncryption());
|
||||
}
|
||||
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
||||
|
|
@ -842,7 +855,7 @@ public class XmppConnectionService extends Service {
|
|||
message.setEncryption(conversation.getNextEncryption());
|
||||
}
|
||||
if (conversation.getCaption() != null) {
|
||||
message.appendBody(conversation.getCaption().getBody() + " ");
|
||||
message.appendBody(conversation.getCaption() + " ");
|
||||
message.setEncryption(conversation.getNextEncryption());
|
||||
}
|
||||
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
||||
|
|
@ -1845,8 +1858,11 @@ public class XmppConnectionService extends Service {
|
|||
toggleForegroundService();
|
||||
rescanStickers();
|
||||
cleanupCache();
|
||||
|
||||
internalPingExecutor.scheduleWithFixedDelay(
|
||||
this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
|
||||
storyRetractionExecutor.scheduleWithFixedDelay(this::retractOldStories, 1, 60, TimeUnit.MINUTES);
|
||||
storyCacheExecutor.scheduleWithFixedDelay(this::cleanupStoryCache, 1, 1, TimeUnit.HOURS);
|
||||
final SharedPreferences sharedPreferences =
|
||||
androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(
|
||||
|
|
@ -1938,6 +1954,8 @@ public class XmppConnectionService extends Service {
|
|||
destroyed = false;
|
||||
fileObserver.stopWatching();
|
||||
internalPingExecutor.shutdown();
|
||||
storyRetractionExecutor.shutdown();
|
||||
storyCacheExecutor.shutdown();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
|
@ -4199,7 +4217,8 @@ public class XmppConnectionService extends Service {
|
|||
&& this.mOnUpdateBlocklist.isEmpty()
|
||||
&& this.mOnShowErrorToasts.isEmpty()
|
||||
&& this.onJingleRtpConnectionUpdate.isEmpty()
|
||||
&& this.mOnKeyStatusUpdated.isEmpty());
|
||||
&& this.mOnKeyStatusUpdated.isEmpty()
|
||||
&& this.mOnStoriesUpdates.isEmpty());
|
||||
}
|
||||
|
||||
private void switchToForeground() {
|
||||
|
|
@ -6347,6 +6366,31 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
private final Set<OnCallLogUpdated> mOnCallLogUpdated =
|
||||
Collections.newSetFromMap(new WeakHashMap<OnCallLogUpdated, Boolean>());
|
||||
|
||||
public void setOnCallLogUpdatedListener(OnCallLogUpdated listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
this.mOnCallLogUpdated.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnCallLogUpdatedListener(OnCallLogUpdated listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
this.mOnCallLogUpdated.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateCallLogUi() {
|
||||
for (OnCallLogUpdated listener : threadSafeList(this.mOnCallLogUpdated)) {
|
||||
listener.onCallLogUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnCallLogUpdated {
|
||||
void onCallLogUpdated();
|
||||
}
|
||||
|
||||
public void notifyJingleRtpConnectionUpdate(
|
||||
final Account account,
|
||||
final Jid with,
|
||||
|
|
@ -6814,7 +6858,7 @@ public class XmppConnectionService extends Service {
|
|||
final var packet =
|
||||
mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
|
||||
|
||||
final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
|
||||
final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message, 1, 2)) + "\n\n";
|
||||
final var body = quote + String.join(" ", newReactions);
|
||||
if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
|
||||
FILE_ATTACHMENT_EXECUTOR.execute(() -> {
|
||||
|
|
@ -7758,4 +7802,626 @@ public class XmppConnectionService extends Service {
|
|||
});
|
||||
}
|
||||
|
||||
public void publishStory(final Account account, final String url, final String type, final String title, final UiCallback<Void> callback) {
|
||||
publishStory(account, url, type, title, true, callback);
|
||||
}
|
||||
|
||||
private void publishStory(final Account account, final String url, final String type, final String title, boolean retry, final UiCallback<Void> callback) {
|
||||
if (account.getStatus() != Account.State.ONLINE) {
|
||||
if (callback != null) {
|
||||
callback.error(R.string.not_connected_try_again, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// This is the corrected publish request. It sends NO configuration options.
|
||||
final Iq packet = getIqGenerator().publishStory(account, url, type, title, null);
|
||||
sendIqPacket(account, packet, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
if (callback != null) {
|
||||
callback.success(null);
|
||||
}
|
||||
} else if (retry && PublishOptions.preconditionNotMet(response)) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stories node does not exist. creating it");
|
||||
// The node does not exist. Now we create it with the correct configuration.
|
||||
final Iq createRequest = getIqGenerator().createStoriesNode();
|
||||
createRequest.setTo(account.getJid().asBareJid());
|
||||
sendIqPacket(account, createRequest, createResponse -> {
|
||||
if (createResponse.getType() == Iq.Type.RESULT) {
|
||||
// Node created. Now, retry publishing the story ONE more time, without the retry/create logic.
|
||||
publishStory(account, url, type, title, false, callback);
|
||||
} else {
|
||||
Log.e(Config.LOGTAG, "Failed to create stories node: " + createResponse);
|
||||
if (callback != null) {
|
||||
callback.error(R.string.error_publish_avatar_server_reject, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.e(Config.LOGTAG, "Failed to publish story: " + response);
|
||||
if (callback != null) {
|
||||
callback.error(R.string.error_publish_avatar_server_reject, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void uploadFileForUrl(final Account account, final android.net.Uri uri, final String mimeType, final UiCallback<String> callback) {
|
||||
if (account == null) {
|
||||
callback.error(R.string.no_active_account, null);
|
||||
return;}
|
||||
|
||||
final DownloadableFile file = getFileBackend().getTemporaryFile(mimeType);
|
||||
|
||||
Runnable runnable = () -> {
|
||||
try {
|
||||
if (mimeType != null && mimeType.startsWith("video/")) {
|
||||
startOngoingVideoTranscodingForegroundNotification();try {
|
||||
final AtomicBoolean transcodeFailed = new AtomicBoolean(false);
|
||||
final TranscoderListener listener = new TranscoderListener() {
|
||||
@Override
|
||||
public void onTranscodeProgress(double progress) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTranscodeCompleted(int successCode) {
|
||||
Log.d(Config.LOGTAG, "transcoding successful for story");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTranscodeCanceled() {
|
||||
Log.d(Config.LOGTAG, "transcoding canceled for story");
|
||||
transcodeFailed.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTranscodeFailed(@NonNull Throwable exception) {
|
||||
Log.w(Config.LOGTAG, "video transcoding failed for story", exception);
|
||||
transcodeFailed.set(true);
|
||||
}};
|
||||
try {
|
||||
Log.d(Config.LOGTAG, "Attempting to transcode video for story");
|
||||
final boolean highQuality = "720".equals(AttachFileToConversationRunnable.getVideoCompression(this));
|
||||
Future<Void> future = Transcoder.into(file.getAbsolutePath())
|
||||
.addDataSource(this, uri)
|
||||
.setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
|
||||
.setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
|
||||
.setListener(listener)
|
||||
.transcode();
|
||||
future.get();
|
||||
} catch (Exception e) {
|
||||
Log.w(Config.LOGTAG, "video transcoding setup failed for story", e);
|
||||
transcodeFailed.set(true);
|
||||
}
|
||||
|
||||
if (transcodeFailed.get() || file.getSize() == 0) {
|
||||
Log.d(Config.LOGTAG, "transcoding failed. falling back to copying original for story");
|
||||
if (file.exists() && !file.delete()) {
|
||||
Log.w(Config.LOGTAG, "could not delete failed transcode file");
|
||||
}
|
||||
getFileBackend().copyFileToPrivateStorage(file, uri);
|
||||
} else {
|
||||
long originalSize = FileBackend.getFileSize(this, uri);
|
||||
if (originalSize > 0 && file.getSize() >= originalSize) {
|
||||
Log.d(Config.LOGTAG, "transcoded file was not smaller. using original for story");
|
||||
if (file.exists() && !file.delete()) {
|
||||
Log.w(Config.LOGTAG, "could not delete failed transcode file");
|
||||
}
|
||||
getFileBackend().copyFileToPrivateStorage(file, uri);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "using transcoded video for story upload");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stopOngoingVideoTranscodingForegroundNotification();
|
||||
}
|
||||
} else if (mimeType != null && mimeType.startsWith("image/")) {
|
||||
getFileBackend().copyImageToPrivateStorage(file, uri);
|
||||
} else {
|
||||
getFileBackend().copyFileToPrivateStorage(file, uri);
|
||||
}
|
||||
} catch (FileBackend.ImageCompressionException e) {
|
||||
Log.d(Config.LOGTAG, "unable to compress image for story. falling back to file transfer", e);
|
||||
try {
|
||||
getFileBackend().copyFileToPrivateStorage(file, uri);
|
||||
} catch (FileBackend.FileCopyException ex) {
|
||||
callback.error(ex.getResId(), null);
|
||||
return;
|
||||
}
|
||||
} catch (final FileBackend.FileCopyException e) {
|
||||
callback.error(e.getResId(), null);
|
||||
return;
|
||||
}
|
||||
|
||||
UiCallback<String> wrapperCallback = new UiCallback<String>() {
|
||||
@Override
|
||||
public void success(String url) {
|
||||
try {
|
||||
callback.success(url);
|
||||
} finally {
|
||||
getFileBackend().deleteFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, String object) {
|
||||
try {
|
||||
callback.error(errorCode, object);
|
||||
} finally {
|
||||
getFileBackend().deleteFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(android.app.PendingIntent pi, String object) {
|
||||
try {
|
||||
callback.userInputRequired(pi, object);
|
||||
} finally {
|
||||
getFileBackend().deleteFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
mHttpConnectionManager.createNewUploadConnection(file, account, false, wrapperCallback);
|
||||
};
|
||||
|
||||
FILE_ATTACHMENT_EXECUTOR.execute(runnable);
|
||||
}
|
||||
|
||||
private final List<eu.siacs.conversations.entities.Story> stories = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
private final Set<OnStoriesUpdate> mOnStoriesUpdates =
|
||||
java.util.Collections.newSetFromMap(new java.util.WeakHashMap<>());
|
||||
|
||||
public List<eu.siacs.conversations.entities.Story> getStories() {
|
||||
return this.stories;
|
||||
}
|
||||
|
||||
public void onStoryReceived(eu.siacs.conversations.entities.Story story) {
|
||||
if (story == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < stories.size(); ++i) {
|
||||
if (stories.get(i).getUuid().equals(story.getUuid())) {
|
||||
stories.set(i, story);
|
||||
updateStoriesUi();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.stories.add(story);
|
||||
java.util.Collections.sort(stories, (a, b) -> Long.compare(b.getPublished(), a.getPublished()));
|
||||
updateStoriesUi();
|
||||
}
|
||||
|
||||
public void setOnStoriesUpdateListener(OnStoriesUpdate listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
this.mOnStoriesUpdates.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnStoriesUpdateListener(OnStoriesUpdate listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
this.mOnStoriesUpdates.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStoriesUi() {
|
||||
for (OnStoriesUpdate listener : threadSafeList(this.mOnStoriesUpdates)) {
|
||||
listener.onStoriesUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnStoriesUpdate {
|
||||
void onStoriesUpdate();
|
||||
}
|
||||
|
||||
public void fetchStories(Account account, Contact contact) {
|
||||
if (account.getStatus() != Account.State.ONLINE) {
|
||||
return;
|
||||
}
|
||||
Log.d(Config.LOGTAG, "fetching stories for " + contact.getJid());
|
||||
Iq request = getIqGenerator().retrieveStories(contact.getJid());
|
||||
sendIqPacket(account, request, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
|
||||
if (pubsub != null) {
|
||||
final List<eu.siacs.conversations.entities.Story> stories = eu.siacs.conversations.entities.Story.parseFromPubSub(pubsub, contact.getJid());
|
||||
for (eu.siacs.conversations.entities.Story story : stories) {
|
||||
onStoryReceived(story);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void retractStory(Account account, String storyId, final UiCallback<Void> callback) {
|
||||
Iq iq = getIqGenerator().deleteItem(Namespace.PUBSUB_STORIES, storyId);
|
||||
this.sendIqPacket(account, iq, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
final List<eu.siacs.conversations.entities.Story> newStories = new ArrayList<>(stories);
|
||||
for (Iterator<eu.siacs.conversations.entities.Story> iterator = newStories.iterator(); iterator.hasNext(); ) {
|
||||
if (iterator.next().getUuid().equals(storyId)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.stories.clear();
|
||||
this.stories.addAll(newStories);
|
||||
updateStoriesUi();
|
||||
if (callback != null) {
|
||||
callback.success(null);
|
||||
}
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.error(R.string.error_deleting_story, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onStoryRetracted(String storyId) {
|
||||
if (storyId == null) {
|
||||
return;
|
||||
}
|
||||
Story storyToRemove = null;
|
||||
for (final Story story : this.stories) {
|
||||
if (story.getUuid().equals(storyId)) {
|
||||
storyToRemove = story;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (storyToRemove != null) {
|
||||
this.stories.remove(storyToRemove);
|
||||
updateStoriesUi();
|
||||
Log.d(Config.LOGTAG, "Retracted story with id: " + storyId);
|
||||
}
|
||||
}
|
||||
|
||||
public void retractOldStories() {
|
||||
final long twentyFourHoursAgo = System.currentTimeMillis() - 86400000;
|
||||
final Map<Jid, Account> onlineAccounts = new HashMap<>();
|
||||
for (final Account account : getAccounts()) {
|
||||
if (account.isOnlineAndConnected()) {
|
||||
onlineAccounts.put(account.getJid().asBareJid(), account);
|
||||
}
|
||||
}
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (final Story story : this.stories) {
|
||||
if (story.getPublished() < twentyFourHoursAgo) {
|
||||
final Account owner = onlineAccounts.get(story.getContact());
|
||||
if (owner != null) {
|
||||
Log.d(Config.LOGTAG, "Retracting old story from account: " + owner.getJid().asBareJid());
|
||||
retractStory(owner, story.getUuid(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupStoryCache() {
|
||||
Log.d(Config.LOGTAG, "Cleaning up story cache");
|
||||
final File storyCacheDir = getFileBackend().getStoryCacheDirectory();
|
||||
if (storyCacheDir.exists()) {
|
||||
final long twentyFourHoursAgo = System.currentTimeMillis() - 86400000;
|
||||
final File[] files = storyCacheDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.lastModified() < twentyFourHoursAgo) {
|
||||
if (file.delete()) {
|
||||
Log.d(Config.LOGTAG, "Deleted old story cache file: " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateContact(final Contact contact) {
|
||||
// First, persist the change in the database
|
||||
if (databaseBackend.updateContact(contact)) {
|
||||
// Then, find the live account and contact objects
|
||||
final Account account = findAccountByUuid(contact.getAccount().getUuid());
|
||||
if (account != null) {
|
||||
final Contact rosterContact = account.getRoster().getContact(contact.getJid());
|
||||
// Update the live contact object with the new setting
|
||||
if (rosterContact != null) {
|
||||
rosterContact.setCallsDisabled(contact.areCallsDisabled());
|
||||
// Now, notify the UI about the change.
|
||||
// This will cause activities like ConversationActivity to redraw their menus
|
||||
// and hide/show the call button as appropriate.
|
||||
updateConversationUi();
|
||||
updateConversationUi(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchPubsubItems(final Jid server, final String node, final OnPubsubItemsFetched callback) {
|
||||
Account account = null;
|
||||
if (server != null) {
|
||||
account = findAccountByJid(server);
|
||||
}
|
||||
if (account == null) {
|
||||
for (Account acc : getAccounts()) {
|
||||
if (acc.isOnlineAndConnected()) {
|
||||
account = acc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (account == null) {
|
||||
if (callback != null) {
|
||||
callback.onPubsubItemsFetchFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Iq request = getIqGenerator().retrievePubsubItems(server, node);
|
||||
sendIqPacket(account, request, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
|
||||
if (pubsub != null && callback != null) {
|
||||
callback.onPubsubItemsFetched(pubsub.toString());
|
||||
} else if (callback != null) {
|
||||
callback.onPubsubItemsFetchFailed();
|
||||
}
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.onPubsubItemsFetchFailed();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface OnPubsubItemsFetched {
|
||||
void onPubsubItemsFetched(String feed);
|
||||
|
||||
void onPubsubItemsFetchFailed();
|
||||
}
|
||||
|
||||
public void publishPost(final Account account, final String title, final String content, final String attachmentUrl, final String attachmentType, final String postId, final OnPostPublished callback) {
|
||||
if (account == null) {
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isEdit = postId != null;
|
||||
final String idToPublish = isEdit ? postId : UUID.randomUUID().toString();
|
||||
|
||||
final Runnable publicationRunnable = () -> {
|
||||
final Iq request = getIqGenerator().publishPost(account, title, content, attachmentUrl, attachmentType, idToPublish);
|
||||
sendIqPacket(account, request, response2 -> {
|
||||
if (response2.getType() == Iq.Type.RESULT) {
|
||||
if (!isEdit) {
|
||||
sendIqPacket(account, getIqGenerator().createCommentsNode(idToPublish), response3 -> {
|
||||
if (response3.getType() != Iq.Type.RESULT) {
|
||||
Log.d(Config.LOGTAG, "could not create comments node for post " + idToPublish + ". " + response3);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.onPostPublished();
|
||||
}
|
||||
} else {
|
||||
Log.e(Config.LOGTAG, "Could not publish post. Server responded with: " + response2);
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!isEdit) {
|
||||
final Iq createRequest = getIqGenerator().createSocialFeedNode();
|
||||
sendIqPacket(account, createRequest, response -> {
|
||||
if (response.getType() != Iq.Type.RESULT) {
|
||||
Element error = response.findChild("error");
|
||||
if (error == null || !error.hasChild("conflict")) {
|
||||
Log.d(Config.LOGTAG, "could not create social feed node " + response);
|
||||
}
|
||||
}
|
||||
publicationRunnable.run();
|
||||
});
|
||||
} else {
|
||||
publicationRunnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
public void publishComment(final Account account, final String nodeUri, final String title, final OnPostPublished callback) {
|
||||
if (account == null) {
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Jid to;
|
||||
final String targetNode;
|
||||
try {
|
||||
final eu.siacs.conversations.utils.XmppUri uri = new eu.siacs.conversations.utils.XmppUri(nodeUri);
|
||||
to = uri.getJid();
|
||||
targetNode = uri.getParameter("node");
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "Invalid URI in publishComment: " + nodeUri, e);
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (to == null || targetNode == null) {
|
||||
Log.e(Config.LOGTAG, "Could not determine target for publishing comment");
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final Iq createRequest = getIqGenerator().createCommentsNode(targetNode);
|
||||
createRequest.setTo(to); // Comments node might be on a different server
|
||||
sendIqPacket(account, createRequest, response -> {
|
||||
if (response.getType() != Iq.Type.RESULT) {
|
||||
Element error = response.findChild("error");
|
||||
if (error == null || !error.hasChild("conflict")) {
|
||||
Log.d(Config.LOGTAG, "could not create comments node " + response);
|
||||
}
|
||||
}
|
||||
final Iq publishRequest = getIqGenerator().publishComment(account, targetNode, title);
|
||||
publishRequest.setTo(to);
|
||||
sendIqPacket(account, publishRequest, publishResponse -> {
|
||||
if (publishResponse.getType() == Iq.Type.RESULT) {
|
||||
if (callback != null) {
|
||||
callback.onPostPublished();
|
||||
}
|
||||
} else {
|
||||
Log.e(Config.LOGTAG, "Could not publish comment. Server responded with: " + publishResponse);
|
||||
if (callback != null) {
|
||||
callback.onPostPublishFailed();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void retractPost(final Account account, final String node, final String id, final OnPostRetracted callback) {
|
||||
if (account == null) {
|
||||
if (callback != null) {
|
||||
callback.onPostRetractionFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Iq request = getIqGenerator().retractPost(node, id);
|
||||
sendIqPacket(account, request, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
if (callback != null) {
|
||||
callback.onPostRetracted(id);
|
||||
}
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.onPostRetractionFailed();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void retractPost(final Account account, final Jid to, final String node, final String id, final OnPostRetracted callback) {
|
||||
final Iq packet = getIqGenerator().retractPost(node, id);
|
||||
packet.setTo(to);
|
||||
this.sendIqPacket(account, packet, response -> {
|
||||
if (response.getType() == Iq.Type.RESULT) {
|
||||
if (callback != null) {
|
||||
callback.onPostRetracted(id);
|
||||
}
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.onPostRetractionFailed();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface OnPostPublished {
|
||||
void onPostPublished();
|
||||
void onPostPublishFailed();
|
||||
}
|
||||
|
||||
public interface OnPostReceived {
|
||||
void onPostReceived(Post post);
|
||||
}
|
||||
|
||||
public interface OnPostRetracted {
|
||||
void onPostRetracted(String postId);
|
||||
void onPostRetractionFailed();
|
||||
}
|
||||
|
||||
private final Set<OnPostReceived> mOnPostReceivedListeners =
|
||||
java.util.Collections.newSetFromMap(new java.util.WeakHashMap<>());
|
||||
private final Set<OnPostRetracted> mOnPostRetractedListeners =
|
||||
java.util.Collections.newSetFromMap(new java.util.WeakHashMap<>());
|
||||
|
||||
|
||||
public void addOnPostReceivedListener(OnPostReceived listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
mOnPostReceivedListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnPostReceivedListener(OnPostReceived listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
mOnPostReceivedListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void addOnPostRetractedListener(OnPostRetracted listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
mOnPostRetractedListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnPostRetractedListener(OnPostRetracted listener) {
|
||||
synchronized (LISTENER_LOCK) {
|
||||
mOnPostRetractedListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPostReceived(Post post, Account account) {
|
||||
if (post == null || account == null) {
|
||||
return;
|
||||
}
|
||||
databaseBackend.createPost(post, account);
|
||||
for (OnPostReceived listener : threadSafeList(mOnPostReceivedListeners)) {
|
||||
listener.onPostReceived(post);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPostRetracted(String postId) {
|
||||
if (postId == null) {
|
||||
return;
|
||||
}
|
||||
databaseBackend.deletePost(postId);
|
||||
for (OnPostRetracted listener : threadSafeList(mOnPostRetractedListeners)) {
|
||||
listener.onPostRetracted(postId);
|
||||
}
|
||||
}
|
||||
|
||||
public void subscribeTo(final Account account, final Jid to, final String node, final Consumer<Iq> callback) {
|
||||
final Iq iq = getIqGenerator().generateSubscriptionIq(to.asBareJid(), node, account.getJid().asBareJid());
|
||||
sendIqPacket(account, iq, callback);
|
||||
}
|
||||
|
||||
public void unsubscribeFrom(final Account account, final Jid to, final String node, final Consumer<Iq> callback) {
|
||||
final Iq iq = getIqGenerator().generateUnsubscriptionIq(to.asBareJid(), node, account.getJid().asBareJid());
|
||||
sendIqPacket(account, iq, callback);
|
||||
}
|
||||
|
||||
public interface OnCommentReceived {
|
||||
void onCommentReceived(String originalPostUuid, Comment comment);
|
||||
}
|
||||
|
||||
private final List<OnCommentReceived> mOnCommentReceivedListeners = new ArrayList<>();
|
||||
|
||||
public void addOnCommentReceivedListener(OnCommentReceived listener) {
|
||||
synchronized (mOnCommentReceivedListeners) {
|
||||
if (!mOnCommentReceivedListeners.contains(listener)) {
|
||||
mOnCommentReceivedListeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnCommentReceivedListener(OnCommentReceived listener) {
|
||||
synchronized (mOnCommentReceivedListeners) {
|
||||
mOnCommentReceivedListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyOnCommentReceived(String originalPostUuid, Comment comment) {
|
||||
synchronized (mOnCommentReceivedListeners) {
|
||||
for (OnCommentReceived listener : mOnCommentReceivedListeners) {
|
||||
try {
|
||||
listener.onCommentReceived(originalPostUuid, comment);
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "safe to ignore, listener has been removed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
161
src/main/java/eu/siacs/conversations/ui/CallsActivity.java
Normal file
161
src/main/java/eu/siacs/conversations/ui/CallsActivity.java
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityCallsBinding;
|
||||
|
||||
public class CallsActivity extends XmppActivity {
|
||||
|
||||
private ActivityCallsBinding binding;
|
||||
private CallsFragment callsFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_calls);
|
||||
Activities.setStatusAndNavigationBarColors(this, findViewById(android.R.id.content));
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
|
||||
callsFragment = (CallsFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
if (callsFragment == null) {
|
||||
callsFragment = new CallsFragment();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, callsFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
|
||||
bottomNavigationView.setOnItemSelectedListener(item -> {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.chats: {
|
||||
startActivity(new Intent(getApplicationContext(), ConversationsActivity.class));
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.feeds: {
|
||||
Intent i = new Intent(getApplicationContext(), PostsActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.stories: {
|
||||
Intent i = new Intent(getApplicationContext(), StoriesActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.calls: {
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + item.getItemId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setSelectedItemId(R.id.calls);
|
||||
|
||||
if (getBooleanPreference("show_nav_bar", R.bool.show_nav_bar) && getIntent().getBooleanExtra("show_nav_bar", false)) {
|
||||
bottomNavigationView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
bottomNavigationView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBackendConnected() {
|
||||
// Clear missed call notifications and badge when the activity is displayed.
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.getNotificationService().clearMissedCalls();
|
||||
}
|
||||
refreshUiReal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (findViewById(R.id.bottom_navigation).getVisibility() == VISIBLE) {
|
||||
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(intent);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
protected void refreshUiReal() {
|
||||
if (xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
// Show badge for unread message in bottom nav
|
||||
int unreadCount = xmppConnectionService.unreadCount();
|
||||
BottomNavigationView bottomnav = findViewById(R.id.bottom_navigation);
|
||||
var bottomBadge = bottomnav.getOrCreateBadge(R.id.chats);
|
||||
bottomBadge.setNumber(unreadCount);
|
||||
bottomBadge.setVisible(unreadCount > 0);
|
||||
bottomBadge.setHorizontalOffset(20);
|
||||
|
||||
// Show badge for new stories in bottom nav
|
||||
long lastRead = getPreferences().getLong("last_read_story_timestamp", 0);
|
||||
boolean hasNewStories = xmppConnectionService.getStories().stream().anyMatch(s -> s.getPublished() > lastRead);
|
||||
var storiesBadge = bottomnav.getOrCreateBadge(R.id.stories);
|
||||
storiesBadge.setVisible(hasNewStories);
|
||||
|
||||
// Show badge for missed calls in bottom nav
|
||||
boolean hasNewMissedCalls = xmppConnectionService.getNotificationService().hasNewMissedCalls();
|
||||
var callsBadge = bottomnav.getOrCreateBadge(R.id.calls);
|
||||
callsBadge.setVisible(hasNewMissedCalls);
|
||||
|
||||
boolean showNavBar = bottomnav.getVisibility() == VISIBLE;
|
||||
if (actionBar != null) {
|
||||
actionBar.setHomeButtonEnabled(!showNavBar);
|
||||
actionBar.setDisplayHomeAsUpEnabled(!showNavBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_stories, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_settings) {
|
||||
startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
256
src/main/java/eu/siacs/conversations/ui/CallsFragment.java
Normal file
256
src/main/java/eu/siacs/conversations/ui/CallsFragment.java
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
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.CallIntegrationConnectionService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.adapter.CallsAdapter;
|
||||
|
||||
public class CallsFragment extends Fragment implements CallsAdapter.OnCallAgainClickListener, CallsAdapter.OnContactClickListener, XmppConnectionService.OnCallLogUpdated {
|
||||
|
||||
private XmppConnectionService xmppConnectionService;
|
||||
private RecyclerView recyclerView;
|
||||
private TextView emptyView;
|
||||
private CallsAdapter adapter;
|
||||
private final List<Message> calls = new ArrayList<>();
|
||||
private Message mPendingCall;
|
||||
private boolean mCallsLoaded = false;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static final int REQUEST_START_AUDIO_CALL = 0x213;
|
||||
private static final int REQUEST_START_VIDEO_CALL = 0x214;
|
||||
|
||||
private final ServiceConnection mConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
|
||||
xmppConnectionService = binder.getService();
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.setOnCallLogUpdatedListener(CallsFragment.this);
|
||||
if (!mCallsLoaded) {
|
||||
loadCalls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0) {
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.removeOnCallLogUpdatedListener(CallsFragment.this);
|
||||
}
|
||||
xmppConnectionService = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Intent intent = new Intent(getActivity(), XmppConnectionService.class);
|
||||
getActivity().bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.removeOnCallLogUpdatedListener(this);
|
||||
}
|
||||
getActivity().unbindService(mConnection);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_calls, container, false);
|
||||
recyclerView = view.findViewById(R.id.list);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
emptyView = view.findViewById(R.id.empty_view);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void loadCalls() {
|
||||
if (xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
executor.execute(() -> {
|
||||
final List<Message> loadedCalls = getCalls();
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
calls.clear();
|
||||
calls.addAll(loadedCalls);
|
||||
if (recyclerView.getAdapter() == null) {
|
||||
adapter = new CallsAdapter(calls, this, this, xmppConnectionService);
|
||||
recyclerView.setAdapter(adapter);
|
||||
} else {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
updateViewStates();
|
||||
mCallsLoaded = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateViewStates() {
|
||||
if (calls.isEmpty()) {
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
emptyView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
recyclerView.setVisibility(View.VISIBLE);
|
||||
emptyView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Message> getCalls() {
|
||||
List<Message> calls = new ArrayList<>();
|
||||
if (xmppConnectionService == null) {
|
||||
return calls;
|
||||
}
|
||||
for (Conversation conversation : xmppConnectionService.getConversations()) {
|
||||
// Limiting to last 100 messages per conversation for performance
|
||||
calls.addAll(xmppConnectionService.databaseBackend.getMessages(conversation, Message.TYPE_RTP_SESSION, 100));
|
||||
}
|
||||
Collections.sort(calls, (o1, o2) -> Long.compare(o2.getTimeSent(), o1.getTimeSent()));
|
||||
return calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAgainClick(Message call, boolean isVideoCall) {
|
||||
mPendingCall = call;
|
||||
if (isVideoCall) {
|
||||
checkPermissionAndTriggerVideoCall();
|
||||
} else {
|
||||
checkPermissionAndTriggerAudioCall();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPermissionAndTriggerAudioCall() {
|
||||
if (xmppConnectionService.useTorToConnect() || mPendingCall.getConversation().getAccount().isOnion()) {
|
||||
Toast.makeText(getActivity(), R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (xmppConnectionService.useI2PToConnect() || mPendingCall.getConversation().getAccount().isI2P()) {
|
||||
Toast.makeText(getActivity(), R.string.no_i2p_calls, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> permissions;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
permissions = Arrays.asList(Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT);
|
||||
} else {
|
||||
permissions = Collections.singletonList(Manifest.permission.RECORD_AUDIO);
|
||||
}
|
||||
if (hasPermissions(permissions, REQUEST_START_AUDIO_CALL)) {
|
||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPermissionAndTriggerVideoCall() {
|
||||
if (xmppConnectionService.useTorToConnect() || mPendingCall.getConversation().getAccount().isOnion()) {
|
||||
Toast.makeText(getActivity(), R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (xmppConnectionService.useI2PToConnect() || mPendingCall.getConversation().getAccount().isI2P()) {
|
||||
Toast.makeText(getActivity(), R.string.no_i2p_calls, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
final List<String> permissions;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
permissions = Arrays.asList(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA, Manifest.permission.BLUETOOTH_CONNECT);
|
||||
} else {
|
||||
permissions = Arrays.asList(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA);
|
||||
}
|
||||
if (hasPermissions(permissions, REQUEST_START_VIDEO_CALL)) {
|
||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPermissions(List<String> permissions, int requestCode) {
|
||||
final List<String> missingPermissions = new ArrayList<>();
|
||||
for (String permission : permissions) {
|
||||
if (getActivity().checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
missingPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
if (missingPermissions.size() == 0) {
|
||||
return true;
|
||||
} else {
|
||||
requestPermissions(missingPermissions.toArray(new String[0]), requestCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (mPendingCall != null) {
|
||||
if(requestCode == REQUEST_START_AUDIO_CALL) {
|
||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
|
||||
} else if (requestCode == REQUEST_START_VIDEO_CALL) {
|
||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void triggerRtpSession(final String action) {
|
||||
if (xmppConnectionService.getJingleConnectionManager().isBusy()) {
|
||||
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
final Conversation conversation = (Conversation) mPendingCall.getConversation();
|
||||
final Account account = conversation.getAccount();
|
||||
if (account.setOption(Account.OPTION_SOFT_DISABLED, false)) {
|
||||
xmppConnectionService.updateAccount(account);
|
||||
}
|
||||
CallIntegrationConnectionService.placeCall(
|
||||
xmppConnectionService,
|
||||
account,
|
||||
conversation.getJid(),
|
||||
RtpSessionActivity.actionToMedia(action));
|
||||
mPendingCall = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactClick(Contact contact) {
|
||||
if (getActivity() instanceof XmppActivity) {
|
||||
((XmppActivity) getActivity()).switchToContactDetails(contact);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallLogUpdated() {
|
||||
loadCalls();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
|
@ -42,6 +43,7 @@ import de.monocles.chat.Util;
|
|||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
|
|
@ -99,6 +101,8 @@ import eu.siacs.conversations.xmpp.Jid;
|
|||
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
|
||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -118,6 +122,8 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
protected MenuItem save = null;
|
||||
|
||||
private Contact contact;
|
||||
private MaterialSwitch mDisableCallsSwitch;
|
||||
private MaterialSwitch mFollowFeedSwitch;
|
||||
private final DialogInterface.OnClickListener removeFromRoster =
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
|
|
@ -178,6 +184,9 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
private OnCheckedChangeListener mOnFollowFeedCheckedChange;
|
||||
|
||||
private Jid accountJid;
|
||||
private Jid contactJid;
|
||||
private boolean showDynamicTags = false;
|
||||
|
|
@ -309,6 +318,76 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
populateView();
|
||||
});
|
||||
binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
|
||||
mDisableCallsSwitch = binding.disableCalls;
|
||||
mFollowFeedSwitch = binding.followFeedSwitch;
|
||||
|
||||
this.mOnFollowFeedCheckedChange =
|
||||
(buttonView, isChecked) -> {
|
||||
if (contact != null) {
|
||||
// Disable the switch to show an operation is in progress
|
||||
mFollowFeedSwitch.setEnabled(false);
|
||||
if (isChecked) {
|
||||
xmppConnectionService.subscribeTo(
|
||||
contact.getAccount(),
|
||||
contact.getJid(),
|
||||
Namespace.MICROBLOG,
|
||||
packet -> {
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
// Always re-enable the switch
|
||||
mFollowFeedSwitch.setEnabled(true);
|
||||
if (packet.getType() == Iq.Type.RESULT) {
|
||||
contact.setFollowed(true);
|
||||
xmppConnectionService.updateContact(contact);
|
||||
setResult(Activity.RESULT_OK);
|
||||
} else {
|
||||
// Revert switch on failure, detaching listener to prevent loop
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(null);
|
||||
mFollowFeedSwitch.setChecked(false);
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(
|
||||
this.mOnFollowFeedCheckedChange);
|
||||
Toast.makeText(
|
||||
ContactDetailsActivity.this,
|
||||
R.string
|
||||
.error_subscribing_to_feed,
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
xmppConnectionService.unsubscribeFrom(
|
||||
contact.getAccount(),
|
||||
contact.getJid(),
|
||||
Namespace.MICROBLOG,
|
||||
packet -> {
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
// Always re-enable the switch
|
||||
mFollowFeedSwitch.setEnabled(true);
|
||||
if (packet.getType() == Iq.Type.RESULT) {
|
||||
contact.setFollowed(false);
|
||||
xmppConnectionService.updateContact(contact);
|
||||
setResult(Activity.RESULT_OK);
|
||||
} else {
|
||||
// Revert switch on failure, detaching listener to prevent loop
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(null);
|
||||
mFollowFeedSwitch.setChecked(true);
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(
|
||||
this.mOnFollowFeedCheckedChange);
|
||||
Toast.makeText(
|
||||
ContactDetailsActivity.this,
|
||||
R.string
|
||||
.error_unsubscribing_from_feed,
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(mOnFollowFeedCheckedChange);
|
||||
|
||||
mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
|
||||
this.binding.media.setAdapter(mMediaAdapter);
|
||||
|
|
@ -603,6 +682,15 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
binding.detailsSendPresence.setOnCheckedChangeListener(null);
|
||||
binding.detailsReceivePresence.setOnCheckedChangeListener(null);
|
||||
|
||||
mDisableCallsSwitch.setChecked(contact.areCallsDisabled());
|
||||
mDisableCallsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
contact.setCallsDisabled(isChecked);
|
||||
xmppConnectionService.updateContact(contact);
|
||||
});
|
||||
mFollowFeedSwitch.setVisibility(View.VISIBLE);
|
||||
mFollowFeedSwitch.setOnCheckedChangeListener(mOnFollowFeedCheckedChange);
|
||||
mFollowFeedSwitch.setChecked(contact.isFollowed());
|
||||
|
||||
List<String> statusMessages = contact.getPresences().getStatusMessages();
|
||||
if (statusMessages.isEmpty()) {
|
||||
binding.statusMessage.setVisibility(View.GONE);
|
||||
|
|
@ -685,6 +773,7 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
binding.detailsSendPresence.setVisibility(View.GONE);
|
||||
binding.detailsReceivePresence.setVisibility(View.GONE);
|
||||
binding.statusMessage.setVisibility(View.GONE);
|
||||
mFollowFeedSwitch.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (contact.isBlocked() && !this.showDynamicTags) {
|
||||
|
|
@ -880,6 +969,9 @@ public class ContactDetailsActivity extends OmemoActivity
|
|||
this.binding.recentThreadsWrapper.setVisibility(View.VISIBLE);
|
||||
Util.justifyListViewHeightBasedOnChildren(binding.recentThreads);
|
||||
}
|
||||
if (contact != null) {
|
||||
mFollowFeedSwitch.setChecked(contact.isFollowed());
|
||||
}
|
||||
}
|
||||
|
||||
private void onBadgeClick(final View view) {
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@ public class ConversationFragment extends XmppFragment
|
|||
public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
|
||||
public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
|
||||
public static final int ATTACHMENT_CHOICE_EDIT_PHOTO = 0x0308;
|
||||
private static final int REQUEST_EDIT_BACKGROUND = 9124;
|
||||
|
||||
public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
|
||||
public static final String STATE_CONVERSATION_UUID =
|
||||
|
|
@ -1270,20 +1271,22 @@ public class ConversationFragment extends XmppFragment
|
|||
}
|
||||
|
||||
private void sendMessage(Long sendAt) {
|
||||
if (sendAt != null && sendAt < System.currentTimeMillis()) sendAt = null; // No sending in past plz
|
||||
Editable body = this.binding.textinput.getText();
|
||||
if (mediaPreviewAdapter.getItemCount() > 1 || (mediaPreviewAdapter.getItemCount() == 1 && body == null)) {
|
||||
commitAttachments();
|
||||
return;
|
||||
if (sendAt != null && sendAt < System.currentTimeMillis()) {
|
||||
sendAt = null; // No sending in past plz
|
||||
}
|
||||
if (body == null) body = new SpannableStringBuilder("");
|
||||
Editable body = this.binding.textinput.getText();
|
||||
if (body == null) {
|
||||
body = new SpannableStringBuilder("");
|
||||
}
|
||||
final Conversation conversation = this.conversation;
|
||||
final boolean hasAttachments = mediaPreviewAdapter.getItemCount() > 0;
|
||||
final boolean hasSubject = binding.textinputSubject.getText().length() > 0;
|
||||
|
||||
if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
|
||||
Toast.makeText(activity, activity.getString(R.string.message_is_too_long), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
final Conversation conversation = this.conversation;
|
||||
final boolean hasSubject = binding.textinputSubject.getText().length() > 0;
|
||||
if (conversation == null || (body.length() == 0 && mediaPreviewAdapter.getItemCount() == 0 && (conversation.getThread() == null || !hasSubject))) {
|
||||
if (conversation == null || (body.length() == 0 && !hasAttachments && (conversation.getThread() == null || !hasSubject))) {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
binding.textSendButton.showContextMenu(0, 0);
|
||||
} else {
|
||||
|
|
@ -1294,16 +1297,29 @@ public class ConversationFragment extends XmppFragment
|
|||
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_TEXT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAttachments) {
|
||||
conversation.setCaption(body.toString());
|
||||
commitAttachments();
|
||||
messageSent();
|
||||
return;
|
||||
}
|
||||
|
||||
final Message message;
|
||||
if (conversation.getCorrectingMessage() == null) {
|
||||
boolean attention = false;
|
||||
if (Pattern.compile("\\A@here\\s.*").matcher(body).find()) {
|
||||
attention = true;
|
||||
body.delete(0, 6);
|
||||
while (body.length() > 0 && Character.isWhitespace(body.charAt(0))) body.delete(0, 1);
|
||||
while (body.length() > 0 && Character.isWhitespace(body.charAt(0))) {
|
||||
body.delete(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation.getReplyTo() != null) {
|
||||
if (Emoticons.isEmoji(body.toString().replaceAll("\\s", "")) && conversation.getNextCounterpart() == null && !conversation.getReplyTo().isPrivateMessage()) {
|
||||
if (((activity.getBooleanPreference("allow_unencrypted_reactions", R.bool.allow_unencrypted_reactions) && conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL) || conversation.getNextEncryption() == Message.ENCRYPTION_NONE)
|
||||
&& Emoticons.isEmoji(body.toString().replaceAll("\\s", ""))
|
||||
&& conversation.getNextCounterpart() == null && !conversation.getReplyTo().isPrivateMessage()) {
|
||||
final var aggregated = conversation.getReplyTo().getAggregatedReactions();
|
||||
final ImmutableSet.Builder<String> reactionBuilder = new ImmutableSet.Builder<>();
|
||||
reactionBuilder.addAll(aggregated.ourReactions);
|
||||
|
|
@ -1344,14 +1360,11 @@ public class ConversationFragment extends XmppFragment
|
|||
}
|
||||
}
|
||||
}
|
||||
// Set caption when only one attachment
|
||||
if (mediaPreviewAdapter.getItemCount() == 1) {
|
||||
conversation.setCaption(message);
|
||||
commitAttachments();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (hasSubject) message.setSubject(binding.textinputSubject.getText().toString());
|
||||
|
||||
if (hasSubject) {
|
||||
message.setSubject(binding.textinputSubject.getText().toString());
|
||||
}
|
||||
if (activity.xmppConnectionService != null && activity.xmppConnectionService.getBooleanPreference("show_thread_feature", R.bool.show_thread_feature)) {
|
||||
message.setThread(conversation.getThread());
|
||||
}
|
||||
|
|
@ -1359,9 +1372,12 @@ public class ConversationFragment extends XmppFragment
|
|||
message.addPayload(new Element("attention", "urn:xmpp:attention:0"));
|
||||
}
|
||||
Message.configurePrivateMessage(message);
|
||||
|
||||
} else {
|
||||
message = conversation.getCorrectingMessage();
|
||||
if (hasSubject) message.setSubject(binding.textinputSubject.getText().toString());
|
||||
if (hasSubject) {
|
||||
message.setSubject(binding.textinputSubject.getText().toString());
|
||||
}
|
||||
if (activity.xmppConnectionService != null && activity.xmppConnectionService.getBooleanPreference("show_thread_feature", R.bool.show_thread_feature)) {
|
||||
message.setThread(conversation.getThread());
|
||||
}
|
||||
|
|
@ -1707,7 +1723,29 @@ public class ConversationFragment extends XmppFragment
|
|||
} else {
|
||||
this.postponedActivityResult.push(activityResult);
|
||||
}
|
||||
if (conversation != null && conversation.getUuid() != null) ChatBackgroundHelper.onActivityResult(activity, requestCode, resultCode, data, conversation.getUuid());
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND) {
|
||||
final Uri imageUri = data == null ? null : data.getData();
|
||||
if (imageUri != null) {
|
||||
final Intent editIntent = new Intent(getActivity(), EditActivity.class);
|
||||
editIntent.setData(imageUri);
|
||||
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivityForResult(editIntent, REQUEST_EDIT_BACKGROUND);
|
||||
return;
|
||||
}
|
||||
} else if (requestCode == REQUEST_EDIT_BACKGROUND) {
|
||||
Uri uri = data != null ? (Uri) data.getParcelableExtra(EditActivity.KEY_EDITED_URI) : null;
|
||||
if (uri == null && data != null) {
|
||||
uri = data.getData();
|
||||
}
|
||||
if (uri != null) {
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.setData(uri);
|
||||
if (conversation != null && conversation.getUuid() != null) ChatBackgroundHelper.onActivityResult(activity, ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND, resultCode, resultIntent, conversation.getUuid());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode == ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND) {
|
||||
refresh();
|
||||
|
|
@ -6696,7 +6734,6 @@ public class ConversationFragment extends XmppFragment
|
|||
if (cid == null) {
|
||||
return false;
|
||||
}
|
||||
// Add null checks for activity and the service to prevent crashes.
|
||||
if (activity == null || activity.xmppConnectionService == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -6712,6 +6749,9 @@ public class ConversationFragment extends XmppFragment
|
|||
|
||||
private boolean isAudioCid(Cid cid) {
|
||||
if (cid == null) return false;
|
||||
if (activity == null || activity.xmppConnectionService == null) {
|
||||
return false;
|
||||
}
|
||||
File file = activity.xmppConnectionService.getFileForCid(cid);
|
||||
if (file == null) return false;
|
||||
String lowerFilePath = file.getAbsolutePath();
|
||||
|
|
|
|||
|
|
@ -253,6 +253,17 @@ public class ConversationsActivity extends XmppActivity
|
|||
bottomBadge.setVisible(unreadCount > 0);
|
||||
bottomBadge.setHorizontalOffset(20);
|
||||
|
||||
// Show badge for new stories in bottom nav
|
||||
long lastRead = getPreferences().getLong("last_read_story_timestamp", 0);
|
||||
boolean hasNewStories = xmppConnectionService.getStories().stream().anyMatch(s -> s.getPublished() > lastRead);
|
||||
var storiesBadge = bottomnav.getOrCreateBadge(R.id.stories);
|
||||
storiesBadge.setVisible(hasNewStories);
|
||||
|
||||
// Show badge for missed calls in bottom nav
|
||||
boolean hasNewMissedCalls = xmppConnectionService.getNotificationService().hasNewMissedCalls();
|
||||
var callsBadge = bottomnav.getOrCreateBadge(R.id.calls);
|
||||
callsBadge.setVisible(hasNewMissedCalls);
|
||||
|
||||
final var chatRequestsPref = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
|
||||
final var accountUnreads = new HashMap<Account, Integer>();
|
||||
binding.drawer.apply(dr -> {
|
||||
|
|
@ -1128,16 +1139,23 @@ public class ConversationsActivity extends XmppActivity
|
|||
case R.id.chats -> {
|
||||
return true;
|
||||
}
|
||||
case R.id.contactslist -> {
|
||||
Intent i = new Intent(getApplicationContext(), StartConversationActivity.class);
|
||||
case R.id.feeds -> {
|
||||
Intent i = new Intent(getApplicationContext(), PostsActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.manageaccounts -> {
|
||||
Intent i = new Intent(getApplicationContext(), MANAGE_ACCOUNT_ACTIVITY);
|
||||
case R.id.stories -> {
|
||||
Intent i = new Intent(getApplicationContext(), StoriesActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.calls -> {
|
||||
Intent i = new Intent(getApplicationContext(), CallsActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.Collections2;
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding;
|
||||
|
|
@ -66,7 +66,6 @@ import eu.siacs.conversations.entities.Contact;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.services.QuickConversationsService;
|
||||
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
|
||||
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
|
||||
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
|
||||
|
|
@ -77,12 +76,8 @@ import eu.siacs.conversations.ui.util.ScrollState;
|
|||
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
|
||||
import eu.siacs.conversations.utils.AccountUtils;
|
||||
import eu.siacs.conversations.utils.EasyOnboardingInvite;
|
||||
import eu.siacs.conversations.utils.ThemeHelper;
|
||||
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.LEFT;
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
|
@ -91,8 +86,7 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
|
||||
private static final String STATE_SCROLL_POSITION =
|
||||
ConversationsOverviewFragment.class.getName() + ".scroll_state";
|
||||
|
||||
private final List<Conversation> conversations = new ArrayList<>();
|
||||
private final List<Conversation> conversations = new ArrayList<>();
|
||||
private final PendingItem<Conversation> swipedConversation = new PendingItem<>();
|
||||
private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
|
||||
private FragmentConversationsOverviewBinding binding;
|
||||
|
|
@ -228,7 +222,7 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
|
||||
pendingActionHelper.push(
|
||||
() -> {
|
||||
if (snackbar.isShownOrQueued()) {
|
||||
if (snackbar.isShownOrQueued()) {
|
||||
snackbar.dismiss();
|
||||
}
|
||||
final Conversation conversation = swipedConversation.pop();
|
||||
|
|
@ -338,7 +332,8 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
inflater, R.layout.fragment_conversations_overview, container, false);
|
||||
this.binding.fab.setOnClickListener(
|
||||
(view) -> StartConversationActivity.launch(getActivity()));
|
||||
|
||||
this.binding.fabStartConversation.setOnClickListener(
|
||||
(view) -> StartConversationActivity.launch(getActivity()));
|
||||
this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations);
|
||||
this.conversationsAdapter.setConversationClickListener(
|
||||
(view, conversation) -> {
|
||||
|
|
@ -360,24 +355,29 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
|
||||
AccountUtils.showHideMenuItems(menu);
|
||||
final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite);
|
||||
MenuItem noteToSelf = menu.findItem(R.id.action_note_to_self);
|
||||
easyOnboardInvite.setVisible(EasyOnboardingInvite.anyHasSupport(activity == null ? null : activity.xmppConnectionService));
|
||||
if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.isOnboarding()) {
|
||||
final MenuItem manageAccounts = menu.findItem(R.id.action_accounts);
|
||||
if (manageAccounts != null) manageAccounts.setVisible(false);
|
||||
|
||||
final MenuItem settings = menu.findItem(R.id.action_settings);
|
||||
if (settings != null) settings.setVisible(false);
|
||||
}
|
||||
if (activity == null || activity.xmppConnectionService == null || activity.xmppConnectionService.getAccounts().size() != 1) {
|
||||
noteToSelf.setVisible(false);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
|
||||
AccountUtils.showHideMenuItems(menu);
|
||||
final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite);
|
||||
MenuItem noteToSelf = menu.findItem(R.id.action_note_to_self);
|
||||
easyOnboardInvite.setVisible(EasyOnboardingInvite.anyHasSupport(activity == null ? null : activity.xmppConnectionService));
|
||||
if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.isOnboarding()) {
|
||||
final MenuItem manageAccounts = menu.findItem(R.id.action_accounts);
|
||||
final MenuItem settings = menu.findItem(R.id.action_settings);
|
||||
final MenuItem stories = menu.findItem(R.id.action_stories);
|
||||
final MenuItem calls = menu.findItem(R.id.action_calls);
|
||||
final MenuItem feeds = menu.findItem(R.id.action_feeds);
|
||||
if (manageAccounts != null) manageAccounts.setVisible(false);
|
||||
if (settings != null) settings.setVisible(false);
|
||||
if (stories != null) stories.setVisible(false);
|
||||
if (calls != null) calls.setVisible(false);
|
||||
if (feeds != null) feeds.setVisible(false);
|
||||
}
|
||||
if (activity == null || activity.xmppConnectionService == null || activity.xmppConnectionService.getAccounts().size() != 1) {
|
||||
noteToSelf.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
|
|
@ -486,11 +486,13 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
boolean navBarVisible = activity instanceof ConversationsActivity && ((ConversationsActivity) activity).navigationBarVisible();
|
||||
MenuItem manageAccount = menu.findItem(R.id.action_account);
|
||||
MenuItem manageAccounts = menu.findItem(R.id.action_accounts);
|
||||
MenuItem stories = menu.findItem(R.id.action_stories);
|
||||
MenuItem calls = menu.findItem(R.id.action_calls);
|
||||
MenuItem feeds = menu.findItem(R.id.action_feeds);
|
||||
if (navBarVisible) {
|
||||
manageAccount.setVisible(false);
|
||||
manageAccounts.setVisible(false);
|
||||
stories.setVisible(false);
|
||||
calls.setVisible(false);
|
||||
feeds.setVisible(false);
|
||||
} else {
|
||||
AccountUtils.showHideMenuItems(menu);
|
||||
}
|
||||
|
|
@ -508,9 +510,11 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
|
||||
if (showed) {
|
||||
this.binding.fab.setVisibility(View.GONE);
|
||||
this.binding.fabStartConversation.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.binding.fab.setVisibility(View.VISIBLE);
|
||||
}
|
||||
this.binding.fabStartConversation.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -520,30 +524,39 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onResume()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (MenuDoubleTabUtil.shouldIgnoreTap()) {
|
||||
return false;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
startActivity(new Intent(getActivity(), SearchActivity.class));
|
||||
return true;
|
||||
case R.id.action_easy_invite:
|
||||
selectAccountToStartEasyInvite();
|
||||
return true;
|
||||
case R.id.action_note_to_self:
|
||||
final List<Account> accounts = activity.xmppConnectionService.getAccounts();
|
||||
if (accounts.size() == 1) {
|
||||
final Contact self = new Contact(accounts.get(0).getSelfContact());
|
||||
Conversation conversation = activity.xmppConnectionService.findOrCreateConversation(self.getAccount(), self.getJid(), false, false, null, true, null);
|
||||
SoftKeyboardUtils.hideSoftKeyboard(activity);
|
||||
activity.switchToConversation(conversation);
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (MenuDoubleTabUtil.shouldIgnoreTap()) {
|
||||
return false;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
startActivity(new Intent(getActivity(), SearchActivity.class));
|
||||
return true;
|
||||
case R.id.action_easy_invite:
|
||||
selectAccountToStartEasyInvite();
|
||||
return true;
|
||||
case R.id.action_note_to_self:
|
||||
final List<Account> accounts = activity.xmppConnectionService.getAccounts();
|
||||
if (accounts.size() == 1) {
|
||||
final Contact self = new Contact(accounts.get(0).getSelfContact());
|
||||
Conversation conversation = activity.xmppConnectionService.findOrCreateConversation(self.getAccount(), self.getJid(), false, false, null, true, null);
|
||||
SoftKeyboardUtils.hideSoftKeyboard(activity);
|
||||
activity.switchToConversation(conversation);
|
||||
}
|
||||
return true;
|
||||
case R.id.action_feeds:
|
||||
startActivity(new Intent(getActivity(), PostsActivity.class));
|
||||
return true;
|
||||
case R.id.action_stories:
|
||||
startActivity(new Intent(getActivity(), StoriesActivity.class));
|
||||
return true;
|
||||
case R.id.action_calls:
|
||||
startActivity(new Intent(getActivity(), CallsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
private void selectAccountToStartEasyInvite() {
|
||||
final List<Account> accounts =
|
||||
EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
|
||||
|
|
@ -578,85 +591,86 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
EasyOnboardingInviteActivity.launch(account, activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refresh() {
|
||||
if (binding == null || this.activity == null) {
|
||||
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
|
||||
return;
|
||||
}
|
||||
this.activity.populateWithOrderedConversations(this.conversations);
|
||||
Conversation removed = this.swipedConversation.peek();
|
||||
if (removed != null) {
|
||||
if (removed.isRead(activity == null ? null : activity.xmppConnectionService)) {
|
||||
this.conversations.remove(removed);
|
||||
} else {
|
||||
pendingActionHelper.execute();
|
||||
}
|
||||
}
|
||||
this.conversationsAdapter.notifyDataSetChanged();
|
||||
ScrollState scrollState = pendingScrollState.pop();
|
||||
if (scrollState != null) {
|
||||
setScrollPosition(scrollState);
|
||||
}
|
||||
protected void refresh() {
|
||||
if (binding == null || this.activity == null) {
|
||||
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
|
||||
return;
|
||||
}
|
||||
this.activity.populateWithOrderedConversations(this.conversations);
|
||||
Conversation removed = this.swipedConversation.peek();
|
||||
if (removed != null) {
|
||||
if (removed.isRead(activity == null ? null : activity.xmppConnectionService)) {
|
||||
this.conversations.remove(removed);
|
||||
} else {
|
||||
pendingActionHelper.execute();
|
||||
}
|
||||
}
|
||||
this.conversationsAdapter.notifyDataSetChanged();
|
||||
ScrollState scrollState = pendingScrollState.pop();
|
||||
if (scrollState != null) {
|
||||
setScrollPosition(scrollState);
|
||||
}
|
||||
if (activity.xmppConnectionService != null && activity.xmppConnectionService.isOnboarding()) {
|
||||
binding.fab.setVisibility(View.GONE);
|
||||
binding.fabStartConversation.setVisibility(View.GONE);
|
||||
|
||||
if (activity.xmppConnectionService != null && activity.xmppConnectionService.isOnboarding()) {
|
||||
binding.fab.setVisibility(View.GONE);
|
||||
if (this.conversations.size() == 1) {
|
||||
if (activity instanceof OnConversationSelected) {
|
||||
((OnConversationSelected) activity).onConversationSelected(this.conversations.get(0));
|
||||
} else {
|
||||
Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activity instanceof ConversationsActivity) {
|
||||
boolean showed = ((ConversationsActivity) activity).showNavigationBar();
|
||||
|
||||
if (this.conversations.size() == 1) {
|
||||
if (activity instanceof OnConversationSelected) {
|
||||
((OnConversationSelected) activity).onConversationSelected(this.conversations.get(0));
|
||||
} else {
|
||||
Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activity instanceof ConversationsActivity) {
|
||||
boolean showed = ((ConversationsActivity) activity).showNavigationBar();
|
||||
if (showed) {
|
||||
this.binding.fab.setVisibility(View.GONE);
|
||||
binding.fabStartConversation.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.binding.fab.setVisibility(View.VISIBLE);
|
||||
binding.fabStartConversation.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activity.getPreferences().getBoolean("swipe_to_archive", true)) setupSwipe();
|
||||
|
||||
if (showed) {
|
||||
this.binding.fab.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.binding.fab.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activity.getPreferences().getBoolean("swipe_to_archive", true)) setupSwipe();
|
||||
if (activity.xmppConnectionService == null || binding == null || binding.overviewSnackbar == null) return;
|
||||
binding.overviewSnackbar.setVisibility(View.GONE);
|
||||
for (final var account : activity.xmppConnectionService.getAccounts()) {
|
||||
if (activity.getPreferences().getBoolean("no_mam_pref_warn:" + account.getUuid(), false)) continue;
|
||||
if (account.mamPrefs() != null && !"always".equals(account.mamPrefs().getAttribute("default"))) {
|
||||
binding.overviewSnackbar.setVisibility(View.VISIBLE);
|
||||
binding.overviewSnackbarMessage.setText(R.string.your_account + " " + account.getJid().asBareJid().toString() + " " + R.string.archiving_not_enabled_text);
|
||||
binding.overviewSnackbarAction.setOnClickListener((v) -> {
|
||||
final var prefs = account.mamPrefs();
|
||||
prefs.setAttribute("default", "always");
|
||||
activity.xmppConnectionService.pushMamPreferences(account, prefs);
|
||||
refresh();
|
||||
});
|
||||
|
||||
if (activity.xmppConnectionService == null || binding == null || binding.overviewSnackbar == null) return;
|
||||
binding.overviewSnackbar.setVisibility(View.GONE);
|
||||
for (final var account : activity.xmppConnectionService.getAccounts()) {
|
||||
if (activity.getPreferences().getBoolean("no_mam_pref_warn:" + account.getUuid(), false)) continue;
|
||||
if (account.mamPrefs() != null && !"always".equals(account.mamPrefs().getAttribute("default"))) {
|
||||
binding.overviewSnackbar.setVisibility(View.VISIBLE);
|
||||
binding.overviewSnackbarMessage.setText(R.string.your_account + " " + account.getJid().asBareJid().toString() + " " + R.string.archiving_not_enabled_text);
|
||||
binding.overviewSnackbarAction.setOnClickListener((v) -> {
|
||||
final var prefs = account.mamPrefs();
|
||||
prefs.setAttribute("default", "always");
|
||||
activity.xmppConnectionService.pushMamPreferences(account, prefs);
|
||||
refresh();
|
||||
});
|
||||
|
||||
binding.overviewSnackbarAction.setOnLongClickListener((v) -> {
|
||||
PopupMenu popupMenu = new PopupMenu(getActivity(), v);
|
||||
popupMenu.inflate(R.menu.mam_pref_fix);
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.ignore:
|
||||
final var editor = activity.getPreferences().edit();
|
||||
editor.putBoolean("no_mam_pref_warn:" + account.getUuid(), true).apply();
|
||||
editor.apply();
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
popupMenu.show();
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.overviewSnackbarAction.setOnLongClickListener((v) -> {
|
||||
PopupMenu popupMenu = new PopupMenu(getActivity(), v);
|
||||
popupMenu.inflate(R.menu.mam_pref_fix);
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.ignore:
|
||||
final var editor = activity.getPreferences().edit();
|
||||
editor.putBoolean("no_mam_pref_warn:" + account.getUuid(), true).apply();
|
||||
editor.apply();
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
popupMenu.show();
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setScrollPosition(ScrollState scrollPosition) {
|
||||
if (scrollPosition != null) {
|
||||
|
|
@ -666,4 +680,5 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
|||
scrollPosition.position, scrollPosition.offset);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
368
src/main/java/eu/siacs/conversations/ui/CreatePostActivity.java
Normal file
368
src/main/java/eu/siacs/conversations/ui/CreatePostActivity.java
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityCreatePostBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
||||
public class CreatePostActivity extends XmppActivity {
|
||||
|
||||
private ActivityCreatePostBinding binding;
|
||||
private String inReplyToId;
|
||||
private String inReplyToNode;
|
||||
private String postId;
|
||||
private Uri attachmentUri;
|
||||
private String attachmentType;
|
||||
private Uri mCameraUri;
|
||||
private String accountUuid;
|
||||
private String postTitle;
|
||||
private String postContent;
|
||||
|
||||
private List<Account> onlineAccounts = new ArrayList<>();
|
||||
|
||||
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||
if (isGranted) {
|
||||
openCamera();
|
||||
} else {
|
||||
Toast.makeText(this, R.string.no_camera_permission, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> takePictureLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
attachmentUri = mCameraUri;
|
||||
attachmentType = "image/jpeg";
|
||||
binding.attachmentPreview.setImageURI(attachmentUri);
|
||||
binding.attachmentPreview.setVisibility(View.VISIBLE);
|
||||
binding.attachmentVideoView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> takeVideoLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
attachmentUri = mCameraUri;
|
||||
attachmentType = "video/mp4";
|
||||
binding.attachmentVideoView.setVideoURI(attachmentUri);
|
||||
binding.attachmentVideoView.setOnPreparedListener(mp -> {
|
||||
mp.setLooping(true);
|
||||
binding.attachmentVideoView.start();
|
||||
});
|
||||
binding.attachmentVideoView.setVisibility(View.VISIBLE);
|
||||
binding.attachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<String> attachFileLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.GetContent(),
|
||||
uri -> {
|
||||
if (uri != null) {
|
||||
this.attachmentUri = uri;
|
||||
this.attachmentType = getContentResolver().getType(attachmentUri);
|
||||
if (this.attachmentType != null && this.attachmentType.startsWith("image/")) {
|
||||
binding.attachmentPreview.setImageURI(attachmentUri);
|
||||
binding.attachmentPreview.setVisibility(View.VISIBLE);
|
||||
binding.attachmentVideoView.setVisibility(View.GONE);
|
||||
} else if (this.attachmentType != null && this.attachmentType.startsWith("video/")) {
|
||||
binding.attachmentVideoView.setVideoURI(attachmentUri);
|
||||
binding.attachmentVideoView.setOnPreparedListener(mp -> {
|
||||
mp.setLooping(true);
|
||||
binding.attachmentVideoView.start();
|
||||
});
|
||||
binding.attachmentVideoView.setVisibility(View.VISIBLE);
|
||||
binding.attachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_post);
|
||||
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
inReplyToId = getIntent().getStringExtra("in_reply_to_id");
|
||||
inReplyToNode = getIntent().getStringExtra("in_reply_to_node");
|
||||
postId = getIntent().getStringExtra("post_id");
|
||||
postTitle = getIntent().getStringExtra("post_title");
|
||||
postContent = getIntent().getStringExtra("post_content");
|
||||
accountUuid = getIntent().getStringExtra("account");
|
||||
if (inReplyToNode != null) {
|
||||
setTitle(R.string.add_a_comment);
|
||||
binding.postTitleEditText.setVisibility(View.GONE);
|
||||
binding.postContentEditText.setHint(R.string.comment);
|
||||
binding.attachFileButton.setVisibility(View.GONE);
|
||||
binding.attachImageButton.setVisibility(View.GONE);
|
||||
binding.attachVideoButton.setVisibility(View.GONE);
|
||||
binding.postPreview.setVisibility(View.VISIBLE);
|
||||
binding.postPreviewTitle.setText(postTitle);
|
||||
binding.postPreviewContent.setText(postContent);
|
||||
} else {
|
||||
binding.postTitleEditText.setVisibility(View.VISIBLE);
|
||||
binding.postContentEditText.setHint(R.string.post_content);
|
||||
}
|
||||
if (postId != null) {
|
||||
binding.postTitleEditText.setText(getIntent().getStringExtra("title"));
|
||||
binding.postContentEditText.setText(getIntent().getStringExtra("content"));
|
||||
if (accountUuid != null) {
|
||||
binding.accountSpinner.setVisibility(View.GONE);
|
||||
}
|
||||
final String attachmentUrl = getIntent().getStringExtra("attachment_url");
|
||||
this.attachmentType = getIntent().getStringExtra("attachment_type");
|
||||
if (attachmentUrl != null && this.attachmentType != null) {
|
||||
this.attachmentUri = Uri.parse(attachmentUrl);
|
||||
if (this.attachmentType.startsWith("image/")) {
|
||||
binding.attachmentPreview.setVisibility(View.VISIBLE);
|
||||
Glide.with(this).load(attachmentUrl).into(binding.attachmentPreview);
|
||||
} else if (this.attachmentType.startsWith("video/")) {
|
||||
binding.attachmentVideoView.setVisibility(View.VISIBLE);
|
||||
binding.attachmentVideoView.setVideoURI(Uri.parse(attachmentUrl));
|
||||
binding.attachmentVideoView.setOnPreparedListener(mp -> {
|
||||
mp.setLooping(true);
|
||||
binding.attachmentVideoView.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.publishButton.setOnClickListener(v -> publishPost());
|
||||
binding.attachFileButton.setOnClickListener(v -> attachFileLauncher.launch("*/*"));
|
||||
binding.attachImageButton.setOnClickListener(v -> {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
openCamera();
|
||||
} else {
|
||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
});
|
||||
binding.attachVideoButton.setOnClickListener(v -> {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
openVideoCamera();
|
||||
} else {
|
||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openCamera() {
|
||||
if (xmppConnectionService == null) {
|
||||
Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
mCameraUri = xmppConnectionService.getFileBackend().getTakePhotoUri();
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCameraUri);
|
||||
takePictureLauncher.launch(takePictureIntent);
|
||||
}
|
||||
|
||||
private void openVideoCamera() {
|
||||
if (xmppConnectionService == null) {
|
||||
Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
mCameraUri = xmppConnectionService.getFileBackend().getTakeVideoUri();
|
||||
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCameraUri);
|
||||
takeVideoLauncher.launch(takeVideoIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackendConnected() {
|
||||
if (xmppConnectionService != null && accountUuid == null) {
|
||||
onlineAccounts.clear();
|
||||
List<String> accountJids = new ArrayList<>();
|
||||
for (Account account : xmppConnectionService.getAccounts()) {
|
||||
if (account.isOnlineAndConnected()) {
|
||||
onlineAccounts.add(account);
|
||||
accountJids.add(account.getJid().asBareJid().toString());
|
||||
}
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, accountJids);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
binding.accountSpinner.setAdapter(adapter);
|
||||
|
||||
int persistedPosition = getPersistedItem();
|
||||
if (!onlineAccounts.isEmpty() && persistedPosition < onlineAccounts.size()) {
|
||||
binding.accountSpinner.setSelection(persistedPosition);
|
||||
}
|
||||
|
||||
binding.accountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parentView, View view, int position, long itemId) {
|
||||
setPersistedItem(position);
|
||||
}
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> arg0) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void publishPost() {
|
||||
String title = binding.postTitleEditText.getText().toString();
|
||||
String content = binding.postContentEditText.getText().toString();
|
||||
|
||||
if (title.isEmpty() && content.isEmpty() && attachmentUri == null) {
|
||||
Toast.makeText(this, R.string.title_or_content_or_attachment_required, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (xmppConnectionService != null) {
|
||||
Account selectedAccount;
|
||||
if (accountUuid != null) {
|
||||
selectedAccount = xmppConnectionService.findAccountByUuid(accountUuid);
|
||||
if (selectedAccount == null) {
|
||||
Toast.makeText(this, getString(R.string.account_not_found), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (binding.accountSpinner.getSelectedItemPosition() < 0 || binding.accountSpinner.getSelectedItemPosition() >= onlineAccounts.size()) {
|
||||
Toast.makeText(this, R.string.no_active_account, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
selectedAccount = onlineAccounts.get(binding.accountSpinner.getSelectedItemPosition());
|
||||
}
|
||||
|
||||
if (!selectedAccount.isOnlineAndConnected()) {
|
||||
Toast.makeText(this, R.string.account_not_connected, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inReplyToNode != null) {
|
||||
xmppConnectionService.publishComment(selectedAccount, inReplyToNode, content, new XmppConnectionService.OnPostPublished() {
|
||||
@Override
|
||||
public void onPostPublished() {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(CreatePostActivity.this, R.string.comment_published, Toast.LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPublishFailed() {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(CreatePostActivity.this, R.string.error_publish_comment, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (attachmentUri != null) {
|
||||
final String mimeType = this.attachmentType != null ? this.attachmentType : getContentResolver().getType(attachmentUri);
|
||||
final String scheme = attachmentUri.getScheme();
|
||||
if (scheme != null && (scheme.equals("http") || scheme.equals("https"))) {
|
||||
publish(selectedAccount, title, content, attachmentUri.toString(), mimeType);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.uploading_attachment, Toast.LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
xmppConnectionService.uploadFileForUrl(selectedAccount, attachmentUri, mimeType, new UiCallback<String>() {
|
||||
@Override
|
||||
public void success(String url) {
|
||||
publish(selectedAccount, title, content, url, mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, String object) {
|
||||
runOnUiThread(() -> Toast.makeText(CreatePostActivity.this, errorCode, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(PendingIntent pi, String object) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
publish(selectedAccount, title, content, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(Account account, String title, String content, String attachmentUrl, String attachmentType) {
|
||||
xmppConnectionService.publishPost(account, title, content, attachmentUrl, attachmentType, postId, new XmppConnectionService.OnPostPublished() {
|
||||
@Override
|
||||
public void onPostPublished() {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(CreatePostActivity.this, R.string.post_published, Toast.LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPublishFailed() {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(CreatePostActivity.this, R.string.error_publish_post, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int getPersistedItem() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this).getInt(makePersistedItemKeyName(), 0);
|
||||
}
|
||||
|
||||
protected void setPersistedItem(int position) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(makePersistedItemKeyName(), position).apply();
|
||||
}
|
||||
|
||||
private String makePersistedItemKeyName() {
|
||||
return "create_post_selected_account_position";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -294,6 +294,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
|
|||
if (mListener != null) {
|
||||
try {
|
||||
if (mListener.onEnterJidDialogPositive(accountJid, contactJid, secondary, binding.bookmark.isChecked())) {
|
||||
context.setResult(Activity.RESULT_OK);
|
||||
dialog.dismiss();
|
||||
}
|
||||
} catch (JidError error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
public interface OnSearchPerformed {
|
||||
void onSearchPerformed(String query);
|
||||
}
|
||||
448
src/main/java/eu/siacs/conversations/ui/PostsActivity.java
Normal file
448
src/main/java/eu/siacs/conversations/ui/PostsActivity.java
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityPostsBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.adapter.FollowSuggestionAdapter;
|
||||
import eu.siacs.conversations.ui.adapter.PostsAdapter;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xml.XmlReader;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class PostsActivity extends XmppActivity implements XmppConnectionService.OnPostReceived, XmppConnectionService.OnPostRetracted, OnSearchPerformed {
|
||||
|
||||
private ActivityPostsBinding binding;
|
||||
private PostsAdapter postsAdapter;
|
||||
private List<Post> postList = new ArrayList<>();
|
||||
private List<Post> allPosts = new ArrayList<>();
|
||||
private String mCurrentQuery = "";
|
||||
private SearchView mSearchView;
|
||||
|
||||
private FollowSuggestionAdapter mFollowSuggestionAdapter;
|
||||
private List<Contact> mFollowSuggestions = new ArrayList<>();
|
||||
private boolean mSuggestionsVisible = true;
|
||||
|
||||
private final ActivityResultLauncher<Intent> postResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
loadPosts();
|
||||
}
|
||||
});
|
||||
|
||||
private void toggleSuggestionsVisibility() {
|
||||
mSuggestionsVisible = !mSuggestionsVisible;
|
||||
binding.followSuggestionsList.setVisibility(mSuggestionsVisible ? View.VISIBLE : View.GONE);
|
||||
binding.toggleSuggestionsButton.animate().rotation(mSuggestionsVisible ? -180 : 0).setDuration(300).start();
|
||||
getPreferences(MODE_PRIVATE).edit().putBoolean("suggestions_visible", mSuggestionsVisible).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mSuggestionsVisible = getPreferences(MODE_PRIVATE).getBoolean("suggestions_visible", true);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_posts);
|
||||
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
|
||||
binding.postsList.setLayoutManager(new LinearLayoutManager(this));
|
||||
postsAdapter = new PostsAdapter(this, postList, postResultLauncher, this);
|
||||
binding.postsList.setAdapter(postsAdapter);
|
||||
|
||||
binding.fabCreatePost.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, CreatePostActivity.class);
|
||||
postResultLauncher.launch(intent);
|
||||
});
|
||||
|
||||
binding.swipeContainer.setOnRefreshListener(() -> {
|
||||
loadPosts();
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
});
|
||||
|
||||
binding.followSuggestionsHeader.setOnClickListener(v -> toggleSuggestionsVisibility());
|
||||
binding.toggleSuggestionsButton.setOnClickListener(v -> toggleSuggestionsVisibility());
|
||||
|
||||
BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
|
||||
bottomNavigationView.setOnItemSelectedListener(item -> {
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.chats: {
|
||||
startActivity(new Intent(getApplicationContext(), ConversationsActivity.class));
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.feeds: {
|
||||
return true;
|
||||
}
|
||||
case R.id.stories: {
|
||||
Intent i = new Intent(getApplicationContext(), StoriesActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.calls: {
|
||||
Intent i = new Intent(getApplicationContext(), CallsActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + item.getItemId());
|
||||
}
|
||||
});
|
||||
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.addOnPostReceivedListener(this);
|
||||
xmppConnectionService.addOnPostRetractedListener(this);
|
||||
}
|
||||
if (postList.isEmpty()) {
|
||||
loadPosts();
|
||||
}
|
||||
BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setSelectedItemId(R.id.feeds);
|
||||
|
||||
if (getBooleanPreference("show_nav_bar", R.bool.show_nav_bar) && getIntent().getBooleanExtra("show_nav_bar", false)) {
|
||||
bottomNavigationView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
bottomNavigationView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.removeOnPostReceivedListener(this);
|
||||
xmppConnectionService.removeOnPostRetractedListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackendConnected() {
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.addOnPostReceivedListener(this);
|
||||
xmppConnectionService.addOnPostRetractedListener(this);
|
||||
if (mFollowSuggestionAdapter == null) {
|
||||
mFollowSuggestionAdapter = new FollowSuggestionAdapter(this, xmppConnectionService, mFollowSuggestions);
|
||||
binding.followSuggestionsList.setAdapter(mFollowSuggestionAdapter);
|
||||
}
|
||||
}
|
||||
refreshUiReal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostReceived(final Post post) {
|
||||
runOnUiThread(() -> {
|
||||
if (post == null || post.getId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int existingPostIndex = -1;
|
||||
for (int i = 0; i < allPosts.size(); i++) {
|
||||
Post existingPost = allPosts.get(i);
|
||||
if (post.getId().equals(existingPost.getId())) {
|
||||
existingPostIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingPostIndex != -1) {
|
||||
allPosts.set(existingPostIndex, post);
|
||||
} else {
|
||||
allPosts.add(0, post);
|
||||
}
|
||||
filterAndDisplayPosts(mCurrentQuery);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
if (postList.isEmpty()) {
|
||||
loadPosts();
|
||||
}
|
||||
|
||||
// Show badge for unread message in bottom nav
|
||||
int unreadCount = xmppConnectionService.unreadCount();
|
||||
BottomNavigationView bottomnav = findViewById(R.id.bottom_navigation);
|
||||
var bottomBadge = bottomnav.getOrCreateBadge(R.id.chats);
|
||||
bottomBadge.setNumber(unreadCount);
|
||||
bottomBadge.setVisible(unreadCount > 0);
|
||||
bottomBadge.setHorizontalOffset(20);
|
||||
|
||||
// Show badge for new stories in bottom nav
|
||||
long lastRead = getPreferences().getLong("last_read_story_timestamp", 0);
|
||||
boolean hasNewStories = xmppConnectionService.getStories().stream().anyMatch(s -> s.getPublished() > lastRead);
|
||||
var storiesBadge = bottomnav.getOrCreateBadge(R.id.stories);
|
||||
storiesBadge.setVisible(hasNewStories);
|
||||
|
||||
// Show badge for missed calls in bottom nav
|
||||
boolean hasNewMissedCalls = xmppConnectionService.getNotificationService().hasNewMissedCalls();
|
||||
var callsBadge = bottomnav.getOrCreateBadge(R.id.calls);
|
||||
callsBadge.setVisible(hasNewMissedCalls);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
boolean showNavBar = binding.bottomNavigation.getVisibility() == VISIBLE;
|
||||
if (actionBar != null) {
|
||||
actionBar.setHomeButtonEnabled(!showNavBar);
|
||||
actionBar.setDisplayHomeAsUpEnabled(!showNavBar);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadPosts() {
|
||||
if (xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
allPosts.clear();
|
||||
allPosts.addAll(xmppConnectionService.databaseBackend.getPosts());
|
||||
filterAndDisplayPosts(mCurrentQuery);
|
||||
|
||||
mFollowSuggestions.clear();
|
||||
for(final Account account : xmppConnectionService.getAccounts()) {
|
||||
if(account.isOnlineAndConnected()) {
|
||||
final List<Jid> sourcesToFetch = new ArrayList<>();
|
||||
sourcesToFetch.add(account.getJid().asBareJid());
|
||||
for (Contact contact : account.getRoster().getContacts()) {
|
||||
if (contact.isFollowed()) {
|
||||
sourcesToFetch.add(contact.getJid().asBareJid());
|
||||
} else if (contact.showInRoster()) {
|
||||
mFollowSuggestions.add(contact);
|
||||
}
|
||||
}
|
||||
|
||||
if (mFollowSuggestions.isEmpty()) {
|
||||
binding.followSuggestionsHeader.setVisibility(View.GONE);
|
||||
binding.followSuggestionsList.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.followSuggestionsHeader.setVisibility(View.VISIBLE);
|
||||
binding.followSuggestionsList.setVisibility(mSuggestionsVisible ? View.VISIBLE : View.GONE);
|
||||
binding.toggleSuggestionsButton.setRotation(mSuggestionsVisible ? -180 : 0);
|
||||
}
|
||||
if (mFollowSuggestionAdapter != null) {
|
||||
mFollowSuggestionAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
for (Jid source : sourcesToFetch) {
|
||||
xmppConnectionService.fetchPubsubItems(source, Namespace.MICROBLOG, new XmppConnectionService.OnPubsubItemsFetched() {
|
||||
@Override
|
||||
public void onPubsubItemsFetched(String feedXml) {
|
||||
try {
|
||||
final XmlReader reader = new XmlReader();
|
||||
reader.setInputStream(new java.io.ByteArrayInputStream(feedXml.getBytes()));
|
||||
final Element pubsub = reader.readElement(reader.readTag());
|
||||
final List<Post> newPosts = new ArrayList<>();
|
||||
if (pubsub != null) {
|
||||
final Element items = pubsub.findChild("items");
|
||||
if (items != null) {
|
||||
for (Element item : items.getChildren()) {
|
||||
if ("item".equals(item.getName())) {
|
||||
Element entry = item.findChild("entry", Namespace.ATOM);
|
||||
if (entry != null) {
|
||||
Post p = Post.fromElement(item);
|
||||
newPosts.add(p);
|
||||
xmppConnectionService.databaseBackend.createPost(p, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
for(Post newPost : newPosts) {
|
||||
boolean found = false;
|
||||
for(Post existingPost : allPosts) {
|
||||
if (existingPost.getId() != null && existingPost.getId().equals(newPost.getId())) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
allPosts.add(newPost);
|
||||
}
|
||||
}
|
||||
filterAndDisplayPosts(mCurrentQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(PostsActivity.this, "Error parsing posts.", Toast.LENGTH_SHORT).show();
|
||||
Log.e(Config.LOGTAG,"error parsing posts",e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPubsubItemsFetchFailed() {
|
||||
// Silently ignore for now
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_posts, menu);
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
mSearchView = (SearchView) searchItem.getActionView();
|
||||
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
filterAndDisplayPosts(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
filterAndDisplayPosts(newText);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
filterAndDisplayPosts("");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_refresh) {
|
||||
xmppConnectionService.databaseBackend.clearPosts();
|
||||
postList.clear();
|
||||
postsAdapter.notifyDataSetChanged();
|
||||
loadPosts();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (mSearchView != null && !mSearchView.isIconified()) {
|
||||
mSearchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
if (mCurrentQuery != null && !mCurrentQuery.isEmpty()) {
|
||||
filterAndDisplayPosts("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (findViewById(R.id.bottom_navigation).getVisibility() == VISIBLE) {
|
||||
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(intent);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
@Override
|
||||
public void onPostRetracted(final String postId) {
|
||||
runOnUiThread(() -> {
|
||||
if (postId == null) {
|
||||
return;
|
||||
}
|
||||
allPosts.removeIf(p -> p.getId().equals(postId));
|
||||
filterAndDisplayPosts(mCurrentQuery);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostRetractionFailed() {
|
||||
runOnUiThread(() -> Toast.makeText(this, R.string.error_retract_post, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchPerformed(String query) {
|
||||
if (mSearchView != null) {
|
||||
mSearchView.setIconified(false);
|
||||
mSearchView.setQuery(query, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void filterAndDisplayPosts(String query) {
|
||||
this.mCurrentQuery = query;
|
||||
postList.clear();
|
||||
if (query.isEmpty()) {
|
||||
postList.addAll(allPosts);
|
||||
} else {
|
||||
postList.addAll(allPosts.stream().filter(p -> (p.getContent() != null && p.getContent().contains(query)) || (p.getTitle() != null && p.getTitle().contains(query))).collect(Collectors.toList()));
|
||||
}
|
||||
java.util.Collections.sort(postList, (p1, p2) -> {
|
||||
if (p1.getPublished() == null && p2.getPublished() == null) return 0;
|
||||
if (p1.getPublished() == null) return 1;
|
||||
if (p2.getPublished() == null) return -1;
|
||||
return p2.getPublished().compareTo(p1.getPublished());
|
||||
});
|
||||
postsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (intent != null) {
|
||||
final String postUuid = intent.getStringExtra("post_uuid");
|
||||
if (postUuid != null) {
|
||||
//This is a rough search. A better way would be to scroll to the item
|
||||
filterAndDisplayPosts(postUuid);
|
||||
}
|
||||
final String hashtag = intent.getStringExtra("hashtag");
|
||||
if (hashtag != null) {
|
||||
onSearchPerformed(hashtag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import static android.view.View.VISIBLE;
|
||||
import static eu.siacs.conversations.utils.AccountUtils.MANAGE_ACCOUNT_ACTIVITY;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
|
|
@ -13,7 +10,6 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -60,9 +56,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import de.monocles.chat.FinishOnboarding;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
|
@ -439,30 +432,6 @@ public class StartConversationActivity extends XmppActivity
|
|||
return false;
|
||||
});
|
||||
|
||||
BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
|
||||
bottomNavigationView.setOnItemSelectedListener(item -> {
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.chats -> {
|
||||
startActivity(new Intent(getApplicationContext(), ConversationsActivity.class));
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
case R.id.contactslist -> {
|
||||
return true;
|
||||
}
|
||||
case R.id.manageaccounts -> {
|
||||
Intent i = new Intent(getApplicationContext(), MANAGE_ACCOUNT_ACTIVITY);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
}
|
||||
default ->
|
||||
throw new IllegalStateException("Unexpected value: " + item.getItemId());
|
||||
}
|
||||
});
|
||||
mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false));
|
||||
mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false));
|
||||
binding.speedDial.setOnActionSelectedListener(actionItem -> {
|
||||
|
|
@ -565,15 +534,6 @@ public class StartConversationActivity extends XmppActivity
|
|||
}
|
||||
requestNotificationPermissionIfNeeded();
|
||||
}
|
||||
|
||||
BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setSelectedItemId(R.id.contactslist);
|
||||
|
||||
if (getBooleanPreference("show_nav_bar", R.bool.show_nav_bar) && getIntent().getBooleanExtra("show_nav_bar", false)) {
|
||||
bottomNavigationView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
bottomNavigationView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestNotificationPermissionIfNeeded() {
|
||||
|
|
@ -965,16 +925,10 @@ public class StartConversationActivity extends XmppActivity
|
|||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
boolean res = super.onPrepareOptionsMenu(menu);
|
||||
boolean navBarVisible = binding.bottomNavigation.getVisibility() == VISIBLE;
|
||||
MenuItem manageAccount = menu.findItem(R.id.action_account);
|
||||
MenuItem manageAccounts = menu.findItem(R.id.action_accounts);
|
||||
MenuItem noteToSelf = menu.findItem(R.id.action_note_to_self);
|
||||
if (navBarVisible) {
|
||||
manageAccount.setVisible(false);
|
||||
manageAccounts.setVisible(false);
|
||||
} else {
|
||||
AccountUtils.showHideMenuItems(menu);
|
||||
}
|
||||
AccountUtils.showHideMenuItems(menu);
|
||||
|
||||
if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() != 1) {
|
||||
noteToSelf.setVisible(false);
|
||||
|
|
@ -1199,17 +1153,9 @@ public class StartConversationActivity extends XmppActivity
|
|||
}
|
||||
boolean openConversations =
|
||||
!createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
|
||||
boolean showNavBar = binding.bottomNavigation.getVisibility() == VISIBLE;
|
||||
actionBar.setDisplayHomeAsUpEnabled(openConversations && !showNavBar);
|
||||
actionBar.setDisplayHomeAsUpEnabled(openConversations && !showNavBar);
|
||||
|
||||
// Show badge for unread message in bottom nav
|
||||
int unreadCount = xmppConnectionService.unreadCount();
|
||||
BottomNavigationView bottomnav = findViewById(R.id.bottom_navigation);
|
||||
var bottomBadge = bottomnav.getOrCreateBadge(R.id.chats);
|
||||
bottomBadge.setNumber(unreadCount);
|
||||
bottomBadge.setVisible(unreadCount > 0);
|
||||
bottomBadge.setHorizontalOffset(20);
|
||||
actionBar.setDisplayHomeAsUpEnabled(openConversations);
|
||||
actionBar.setDisplayHomeAsUpEnabled(openConversations);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1543,9 +1489,6 @@ public class StartConversationActivity extends XmppActivity
|
|||
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(intent);
|
||||
if (binding.bottomNavigation.getVisibility() == VISIBLE) {
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
}
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
|
|
|||
456
src/main/java/eu/siacs/conversations/ui/StoriesActivity.java
Normal file
456
src/main/java/eu/siacs/conversations/ui/StoriesActivity.java
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityStoriesBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.medialib.activities.EditActivity;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.adapter.StoryAdapter;
|
||||
import eu.siacs.conversations.ui.util.PendingItem;
|
||||
|
||||
public class StoriesActivity extends XmppActivity implements XmppConnectionService.OnStoriesUpdate {
|
||||
|
||||
private static final int REQUEST_CHOOSE_STORY_IMAGE = 0x2b01;
|
||||
private static final int REQUEST_CAMERA_PERMISSION = 0x2b03;
|
||||
private static final int REQUEST_EDIT_STORY_IMAGE = 0x2b04;
|
||||
private Account mSelectedAccount;
|
||||
private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
|
||||
|
||||
private ActivityStoriesBinding binding;
|
||||
private StoryAdapter storyAdapter;
|
||||
private final List<Story> stories = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_stories);
|
||||
Activities.setStatusAndNavigationBarColors(this, findViewById(android.R.id.content));
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
binding.fabAddStory.setOnClickListener(v -> selectAccountToPublishStory());
|
||||
storyAdapter = new StoryAdapter(this, stories);
|
||||
binding.storiesList.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.storiesList.setAdapter(storyAdapter);
|
||||
|
||||
// Bottom Navigation Setup
|
||||
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
|
||||
bottomNavigationView.setOnItemSelectedListener(item -> {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.chats:
|
||||
startActivity(new Intent(getApplicationContext(), ConversationsActivity.class));
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
case R.id.feeds:
|
||||
Intent i = new Intent(getApplicationContext(), PostsActivity.class);
|
||||
i.putExtra("show_nav_bar", true);
|
||||
startActivity(i);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
case R.id.stories:
|
||||
return true;
|
||||
case R.id.calls:
|
||||
Intent callsIntent = new Intent(getApplicationContext(), CallsActivity.class);
|
||||
callsIntent.putExtra("show_nav_bar", true);
|
||||
startActivity(callsIntent);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
return true;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + item.getItemId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationView.setSelectedItemId(R.id.stories);
|
||||
|
||||
if (getBooleanPreference("show_nav_bar", R.bool.show_nav_bar) && getIntent().getBooleanExtra("show_nav_bar", false)) {
|
||||
bottomNavigationView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
bottomNavigationView.setVisibility(View.GONE);
|
||||
}
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.setOnStoriesUpdateListener(this);
|
||||
getPreferences().edit().putLong("last_read_story_timestamp", System.currentTimeMillis()).apply();
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.removeOnStoriesUpdateListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBackendConnected() {
|
||||
if (xmppConnectionService != null) {
|
||||
xmppConnectionService.setOnStoriesUpdateListener(this);
|
||||
refresh();
|
||||
}
|
||||
refreshUiReal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (findViewById(R.id.bottom_navigation).getVisibility() == VISIBLE) {
|
||||
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
startActivity(intent);
|
||||
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_stories, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_settings) {
|
||||
startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void publish(Uri uri, String mimeType) {
|
||||
if (uri == null || mSelectedAccount == null) {
|
||||
return;
|
||||
}
|
||||
showCreateStoryDialog(uri, mimeType);
|
||||
}
|
||||
|
||||
private void showCreateStoryDialog(Uri uri, String mimeType) {
|
||||
View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_create_story, null);
|
||||
ImageView storyPreviewImage = dialogView.findViewById(R.id.story_preview_image);
|
||||
VideoView storyPreviewVideo = dialogView.findViewById(R.id.story_preview_video);
|
||||
TextView publishInfoText = dialogView.findViewById(R.id.publish_info_text);
|
||||
TextInputEditText titleEditText = dialogView.findViewById(R.id.title_edit_text);
|
||||
|
||||
final TextWatcher textWatcher = new TextWatcher() {
|
||||
private String before;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
before = s.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// No action needed here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (titleEditText.getLineCount() > 20) {
|
||||
titleEditText.removeTextChangedListener(this);
|
||||
titleEditText.setText(before);
|
||||
titleEditText.setSelection(before.length());
|
||||
titleEditText.addTextChangedListener(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
titleEditText.addTextChangedListener(textWatcher);
|
||||
|
||||
long subscriberCount = mSelectedAccount.getRoster().getContacts().stream()
|
||||
.filter(c -> c.getOption(Contact.Options.FROM))
|
||||
.count();publishInfoText.setText(getResources().getQuantityString(R.plurals.publishing_to_x_contacts, (int) subscriberCount, (int) subscriberCount));
|
||||
|
||||
if (mimeType.startsWith("video/")) {
|
||||
storyPreviewVideo.setVisibility(View.VISIBLE);
|
||||
storyPreviewVideo.setVideoURI(uri);
|
||||
storyPreviewVideo.setOnPreparedListener(mp -> {
|
||||
mp.setLooping(true);
|
||||
storyPreviewVideo.start();
|
||||
});
|
||||
} else {
|
||||
storyPreviewImage.setVisibility(View.VISIBLE);
|
||||
Glide.with(this).load(uri).into(storyPreviewImage);
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.add_story_title)
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.publish, (dialog2, which2) -> {
|
||||
final String title = titleEditText.getText().toString();
|
||||
Toast.makeText(this, R.string.uploading_story, Toast.LENGTH_SHORT).show();
|
||||
xmppConnectionService.uploadFileForUrl(mSelectedAccount, uri, mimeType, new UiCallback<String>() {
|
||||
@Override
|
||||
public void success(String url) {
|
||||
xmppConnectionService.publishStory(mSelectedAccount, url, mimeType, title, new UiCallback<Void>() {
|
||||
@Override
|
||||
public void success(Void aVoid) {
|
||||
runOnUiThread(() -> Toast.makeText(StoriesActivity.this, R.string.story_published, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, Void object) {
|
||||
runOnUiThread(() -> Toast.makeText(StoriesActivity.this, errorCode, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(PendingIntent pi, Void object) {
|
||||
// not used
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, String object) {
|
||||
runOnUiThread(() -> Toast.makeText(StoriesActivity.this, errorCode, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(PendingIntent pi, String object) {
|
||||
// not used
|
||||
}
|
||||
});
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == REQUEST_CHOOSE_STORY_IMAGE) {
|
||||
Uri uri;
|
||||
if (data != null && data.getData() != null) {
|
||||
uri = data.getData();
|
||||
} else if (pendingTakePhotoUri.peek() != null) {
|
||||
uri = pendingTakePhotoUri.pop();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (mSelectedAccount != null) {
|
||||
String mimeType = getContentResolver().getType(uri);
|
||||
if (mimeType != null && mimeType.startsWith("video/")) {
|
||||
try {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
retriever.setDataSource(this, uri);
|
||||
String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
long durationMs = 0;
|
||||
if (time != null) {
|
||||
durationMs = Long.parseLong(time);
|
||||
}
|
||||
retriever.release();
|
||||
if (durationMs > 60000) {
|
||||
Toast.makeText(this, R.string.video_too_long_for_story, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
publish(uri, mimeType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "Could not determine video duration", e);
|
||||
Toast.makeText(this, R.string.error_retrieving_video_duration, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(this, EditActivity.class);
|
||||
intent.setData(uri);
|
||||
startActivityForResult(intent, REQUEST_EDIT_STORY_IMAGE);
|
||||
}
|
||||
}
|
||||
} else if (requestCode == REQUEST_EDIT_STORY_IMAGE) {
|
||||
Uri uri = data != null ? (Uri) data.getParcelableExtra(EditActivity.KEY_EDITED_URI) : null;
|
||||
if (uri == null && data != null) {
|
||||
uri = data.getData();
|
||||
}
|
||||
if (uri != null) {
|
||||
String mimeType = data.getType();
|
||||
if (mimeType == null) {
|
||||
mimeType = getContentResolver().getType(uri);
|
||||
}
|
||||
if (mimeType == null) {
|
||||
mimeType = "image/jpeg";
|
||||
}
|
||||
publish(uri, mimeType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pendingTakePhotoUri.pop();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectAccountToPublishStory() {
|
||||
final List<Account> accounts = xmppConnectionService.getAccounts().stream().filter(account -> account.getStatus() == Account.State.ONLINE).collect(Collectors.toList());
|
||||
if (accounts.isEmpty()) {
|
||||
Toast.makeText(this, R.string.no_active_account, Toast.LENGTH_SHORT).show();
|
||||
} else if (accounts.size() == 1) {
|
||||
openStoryImagePicker(accounts.get(0));
|
||||
} else {
|
||||
final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
|
||||
final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
|
||||
alertDialogBuilder.setTitle(R.string.choose_account);
|
||||
final String[] asStrings =
|
||||
accounts.stream().map(a -> a.getJid().asBareJid().toString()).toArray(String[]::new);
|
||||
alertDialogBuilder.setSingleChoiceItems(
|
||||
asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel, null);
|
||||
alertDialogBuilder.setPositiveButton(
|
||||
R.string.ok, (dialog, which) -> openStoryImagePicker(selectedAccount.get()));
|
||||
alertDialogBuilder.create().show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void openStoryImagePicker(Account account) {
|
||||
this.mSelectedAccount = account;
|
||||
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
|
||||
} else {
|
||||
final Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
galleryIntent.setType("image/*,video/*");
|
||||
galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
|
||||
final Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
final Uri takePhotoUri = xmppConnectionService.getFileBackend().getTakePhotoUri();
|
||||
pendingTakePhotoUri.push(takePhotoUri);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, takePhotoUri);
|
||||
|
||||
final Intent videoIntent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
|
||||
final Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.perform_action_with));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{cameraIntent, videoIntent});
|
||||
|
||||
try {
|
||||
startActivityForResult(chooserIntent, REQUEST_CHOOSE_STORY_IMAGE);
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
Toast.makeText(this, R.string.no_application_found, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
openStoryImagePicker(mSelectedAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
// Show badge for unread message in bottom nav
|
||||
int unreadCount = xmppConnectionService.unreadCount();
|
||||
BottomNavigationView bottomnav = findViewById(R.id.bottom_navigation);
|
||||
var bottomBadge = bottomnav.getOrCreateBadge(R.id.chats);
|
||||
bottomBadge.setNumber(unreadCount);
|
||||
bottomBadge.setVisible(unreadCount > 0);
|
||||
bottomBadge.setHorizontalOffset(20);
|
||||
|
||||
// Show badge for new stories in bottom nav
|
||||
long lastRead = getPreferences().getLong("last_read_story_timestamp", 0);
|
||||
boolean hasNewStories = xmppConnectionService.getStories().stream().anyMatch(s -> s.getPublished() > lastRead);
|
||||
var storiesBadge = bottomnav.getOrCreateBadge(R.id.stories);
|
||||
storiesBadge.setVisible(hasNewStories);
|
||||
|
||||
// Show badge for missed calls in bottom nav
|
||||
boolean hasNewMissedCalls = xmppConnectionService.getNotificationService().hasNewMissedCalls();
|
||||
var callsBadge = bottomnav.getOrCreateBadge(R.id.calls);
|
||||
callsBadge.setVisible(hasNewMissedCalls);
|
||||
|
||||
boolean showNavBar = bottomnav.getVisibility() == VISIBLE;
|
||||
if (actionBar != null) {
|
||||
actionBar.setHomeButtonEnabled(!showNavBar);
|
||||
actionBar.setDisplayHomeAsUpEnabled(!showNavBar);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
if (xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
this.stories.clear();
|
||||
long twentyFourHoursAgo = System.currentTimeMillis() - 86400000;
|
||||
this.stories.addAll(
|
||||
this.xmppConnectionService.getStories().stream()
|
||||
.filter(s -> s.getPublished() >= twentyFourHoursAgo)
|
||||
.collect(Collectors.toMap(
|
||||
story -> story.getContact().asBareJid(),
|
||||
story -> story,
|
||||
(a, b) -> a.getPublished() > b.getPublished() ? a : b
|
||||
))
|
||||
.values()
|
||||
);
|
||||
Collections.sort(this.stories, (a, b) -> Long.compare(b.getPublished(), a.getPublished()));
|
||||
storyAdapter.notifyDataSetChanged();
|
||||
|
||||
final boolean hasOnlineAccounts = xmppConnectionService.getAccounts().stream().anyMatch(account -> account.getStatus() == Account.State.ONLINE);
|
||||
|
||||
if (!hasOnlineAccounts && this.stories.isEmpty()) {
|
||||
binding.storiesList.setVisibility(View.GONE);
|
||||
binding.fabAddStory.setVisibility(View.GONE);
|
||||
binding.placeholder.setVisibility(View.VISIBLE);
|
||||
binding.placeholder.setText(R.string.no_active_account_to_show_stories);
|
||||
} else {
|
||||
binding.placeholder.setVisibility(View.GONE);
|
||||
binding.fabAddStory.setVisibility(hasOnlineAccounts ? View.VISIBLE : View.GONE);
|
||||
binding.storiesList.setVisibility(this.stories.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoriesUpdate() {
|
||||
runOnUiThread(this::refresh);
|
||||
}
|
||||
}
|
||||
296
src/main/java/eu/siacs/conversations/ui/StoryFragment.java
Normal file
296
src/main/java/eu/siacs/conversations/ui/StoryFragment.java
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
|
||||
public class StoryFragment extends Fragment {
|
||||
|
||||
private static final String ARG_URL = "url";
|
||||
private static final String ARG_MIME_TYPE = "mime_type";
|
||||
private static final long STORY_DURATION_MS = 6000;
|
||||
|
||||
private OnStoryInteractionListener mListener;
|
||||
private VideoView videoView;
|
||||
private ObjectAnimator currentAnimator;
|
||||
private LinearLayout progressBarContainer;
|
||||
private ArrayList<String> urls;
|
||||
private final Handler videoProgressHandler = new Handler(Looper.getMainLooper());
|
||||
private Runnable videoProgressRunnable;
|
||||
|
||||
public interface OnStoryInteractionListener {
|
||||
void onNextStory();
|
||||
|
||||
void pauseStory();
|
||||
|
||||
void resumeStory();
|
||||
}
|
||||
|
||||
public static StoryFragment newInstance(String url, String mimeType, ArrayList<String> urls) {
|
||||
StoryFragment fragment = new StoryFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ARG_URL, url);
|
||||
args.putString(ARG_MIME_TYPE, mimeType);
|
||||
args.putStringArrayList("urls", urls);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof OnStoryInteractionListener) {
|
||||
mListener = (OnStoryInteractionListener) context;
|
||||
} else {
|
||||
throw new RuntimeException(context.toString() + " must implement OnStoryInteractionListener");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_story, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
view.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
pauseStory();
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
resumeStory();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
progressBarContainer = view.findViewById(R.id.progress_bar_container);
|
||||
if (getArguments() != null) {
|
||||
urls = getArguments().getStringArrayList("urls");
|
||||
}
|
||||
setupProgressBars();
|
||||
loadStory();
|
||||
}
|
||||
|
||||
private void setupProgressBars() {
|
||||
if (urls == null) {
|
||||
return;
|
||||
}
|
||||
progressBarContainer.removeAllViews();
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
ProgressBar progressBar = (ProgressBar) LayoutInflater.from(getContext()).inflate(R.layout.story_progress_bar, progressBarContainer, false);
|
||||
progressBar.setMax(1000);
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) progressBar.getLayoutParams();
|
||||
params.weight = 1;
|
||||
progressBarContainer.addView(progressBar);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadStory() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View view = getView();
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ImageView imageView = view.findViewById(R.id.story_image_view);
|
||||
videoView = view.findViewById(R.id.story_video_view);
|
||||
final ProgressBar progressBar = view.findViewById(R.id.story_progress);
|
||||
|
||||
final Bundle args = getArguments();
|
||||
if (args == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String url = args.getString(ARG_URL);
|
||||
final String mimeType = args.getString(ARG_MIME_TYPE);
|
||||
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
|
||||
final int currentPosition = urls.indexOf(url);
|
||||
|
||||
for (int i = 0; i < currentPosition; i++) {
|
||||
((ProgressBar) progressBarContainer.getChildAt(i)).setProgress(1000);
|
||||
}
|
||||
for (int i = currentPosition; i < urls.size(); i++) {
|
||||
((ProgressBar) progressBarContainer.getChildAt(i)).setProgress(0);
|
||||
}
|
||||
|
||||
|
||||
if (mimeType != null && mimeType.startsWith("video/")) {
|
||||
imageView.setVisibility(View.GONE);
|
||||
videoView.setVisibility(View.VISIBLE);
|
||||
videoView.setOnCompletionListener(mp -> {
|
||||
if (mListener != null) {
|
||||
mListener.onNextStory();
|
||||
}
|
||||
});
|
||||
Glide.with(this)
|
||||
.asFile()
|
||||
.load(url)
|
||||
.into(new CustomTarget<File>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> transition) {
|
||||
if (!isAdded()) return;
|
||||
videoView.setVideoURI(Uri.fromFile(resource));
|
||||
videoView.setOnPreparedListener(mp -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
mp.setLooping(false);
|
||||
videoView.start();
|
||||
final int duration = mp.getDuration();
|
||||
final ProgressBar currentStoryProgressBar = (ProgressBar) progressBarContainer.getChildAt(currentPosition);
|
||||
if (currentStoryProgressBar != null) {
|
||||
videoProgressRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (videoView != null && videoView.isPlaying()) {
|
||||
int currentPosition = videoView.getCurrentPosition();
|
||||
int progress = (int) (((float) currentPosition / duration) * 1000);
|
||||
currentStoryProgressBar.setProgress(progress);
|
||||
videoProgressHandler.postDelayed(this, 50);
|
||||
}
|
||||
}
|
||||
};
|
||||
videoProgressHandler.post(videoProgressRunnable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
super.onLoadFailed(errorDrawable);
|
||||
if (!isAdded()) return;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
Toast.makeText(getContext(), R.string.download_failed_file_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
videoView.setVisibility(View.GONE);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
Glide.with(this)
|
||||
.load(url)
|
||||
.listener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
if (isAdded()) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
Toast.makeText(getContext(), R.string.download_failed_file_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource
|
||||
dataSource, boolean isFirstResource) {
|
||||
if (isAdded()) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
final ProgressBar currentProgressBar = (ProgressBar) progressBarContainer.getChildAt(currentPosition);
|
||||
currentAnimator = ObjectAnimator.ofInt(currentProgressBar, "progress", 0, 1000);
|
||||
currentAnimator.setDuration(STORY_DURATION_MS);
|
||||
currentAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
if (mListener != null) {
|
||||
mListener.onNextStory();
|
||||
}
|
||||
}
|
||||
});
|
||||
currentAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseVideo() {
|
||||
if (videoView != null && videoView.isPlaying()) {
|
||||
videoView.pause();
|
||||
if (videoProgressRunnable != null) {
|
||||
videoProgressHandler.removeCallbacks(videoProgressRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void resumeVideo() {
|
||||
if (videoView != null && !videoView.isPlaying()) {
|
||||
videoView.start();
|
||||
if (videoProgressRunnable != null) {
|
||||
videoProgressHandler.post(videoProgressRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseStory() {
|
||||
if (currentAnimator != null && currentAnimator.isRunning()) {
|
||||
currentAnimator.pause();
|
||||
}
|
||||
mListener.pauseStory();
|
||||
pauseVideo();
|
||||
}
|
||||
|
||||
public void resumeStory() {
|
||||
if (currentAnimator != null && currentAnimator.isPaused()) {
|
||||
currentAnimator.resume();
|
||||
}
|
||||
mListener.resumeStory();
|
||||
resumeVideo();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
if (videoProgressRunnable != null) {
|
||||
videoProgressHandler.removeCallbacks(videoProgressRunnable);
|
||||
}
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
379
src/main/java/eu/siacs/conversations/ui/StoryViewActivity.java
Normal file
379
src/main/java/eu/siacs/conversations/ui/StoryViewActivity.java
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateUtils;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
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.entities.Story;
|
||||
import eu.siacs.conversations.parser.AbstractParser;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.ui.widget.AvatarView;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class StoryViewActivity extends XmppActivity implements StoryFragment.OnStoryInteractionListener {
|
||||
|
||||
public static final String EXTRA_URLS = "urls";
|
||||
public static final String EXTRA_TITLES = "titles";
|
||||
public static final String EXTRA_STORY_IDS = "story_ids";
|
||||
public static final String EXTRA_ACCOUNT = "account";
|
||||
public static final String EXTRA_CONTACT = "contact";
|
||||
public static final String EXTRA_MIME_TYPES = "story_mime_types";
|
||||
|
||||
private ViewPager2 viewPager;
|
||||
private TextView titleView;
|
||||
private View bottomPanel;
|
||||
private AppBarLayout appBarLayout;
|
||||
private AvatarView toolbarAvatar;
|
||||
private TextView toolbarTitle;
|
||||
private TextView toolbarSubtitle;
|
||||
private LinearLayout progressBarContainer;
|
||||
|
||||
private ArrayList<String> urls;
|
||||
private ArrayList<String> titles;
|
||||
private ArrayList<String> storyIds;
|
||||
private ArrayList<String> mimeTypes;
|
||||
private Jid contact;
|
||||
private Account mAccount;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_story_view);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
}
|
||||
|
||||
appBarLayout = findViewById(R.id.app_bar_layout);
|
||||
toolbarAvatar = findViewById(R.id.toolbar_avatar);
|
||||
toolbarTitle = findViewById(R.id.toolbar_title);
|
||||
toolbarSubtitle = findViewById(R.id.toolbar_subtitle);
|
||||
progressBarContainer = findViewById(R.id.progress_bar_container);
|
||||
|
||||
viewPager = findViewById(R.id.view_pager);
|
||||
titleView = findViewById(R.id.story_title_view);
|
||||
titleView.setMovementMethod(new ScrollingMovementMethod());
|
||||
|
||||
bottomPanel = findViewById(R.id.bottom_panel);
|
||||
|
||||
urls = getIntent().getStringArrayListExtra(EXTRA_URLS);
|
||||
titles = getIntent().getStringArrayListExtra(EXTRA_TITLES);
|
||||
storyIds = getIntent().getStringArrayListExtra(EXTRA_STORY_IDS);
|
||||
mimeTypes = getIntent().getStringArrayListExtra(EXTRA_MIME_TYPES);
|
||||
|
||||
try {
|
||||
contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT));
|
||||
} catch (final Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
setupProgressBars();
|
||||
|
||||
StoryPagerAdapter adapter = new StoryPagerAdapter(this);
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
updateUiForPosition(position);
|
||||
}
|
||||
});
|
||||
updateUiForPosition(0);
|
||||
showSystemUi();
|
||||
}
|
||||
|
||||
private void setupProgressBars() {
|
||||
progressBarContainer.removeAllViews();
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
ProgressBar progressBar = (ProgressBar) LayoutInflater.from(this).inflate(R.layout.story_progress_bar, progressBarContainer, false);
|
||||
progressBar.setMax(1000);
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) progressBar.getLayoutParams();
|
||||
params.weight = 1;
|
||||
progressBarContainer.addView(progressBar);
|
||||
}
|
||||
}
|
||||
|
||||
public void onNextStory() {
|
||||
showSystemUi();
|
||||
final int currentItem = viewPager.getCurrentItem();
|
||||
final int nextItem = currentItem + 1;
|
||||
if (nextItem < urls.size()) {
|
||||
viewPager.setCurrentItem(nextItem, false);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUiForPosition(int position) {
|
||||
Contact storyContact = null;
|
||||
if (contact != null && xmppConnectionService != null) {
|
||||
for (Account account : xmppConnectionService.getAccounts()) {
|
||||
if (account.getStatus() == Account.State.ONLINE) {
|
||||
final Contact c = account.getRoster().getContact(contact);
|
||||
if (c != null) {
|
||||
storyContact = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (storyContact == null) {
|
||||
for (Account account : xmppConnectionService.getAccounts()) {
|
||||
final Contact c = account.getRoster().getContact(contact);
|
||||
if (c != null) {
|
||||
storyContact = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String displayName;
|
||||
if (storyContact != null) {
|
||||
displayName = storyContact.getDisplayName();
|
||||
if (mAccount != null) {
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(mAccount, contact, false, false);
|
||||
AvatarWorkerTask.loadAvatar(conversation, toolbarAvatar, R.dimen.muc_avatar_actionbar);
|
||||
}
|
||||
} else if (contact != null) {
|
||||
displayName = contact.asBareJid().toString();
|
||||
} else {
|
||||
displayName = "";
|
||||
}
|
||||
toolbarTitle.setText(displayName);
|
||||
long publishedTimestamp = 0;
|
||||
if (storyIds != null && position < storyIds.size()) {
|
||||
final String currentStoryId = storyIds.get(position);
|
||||
if (xmppConnectionService != null) {
|
||||
long twentyFourHoursAgo = System.currentTimeMillis() - 86400000;
|
||||
for (Story story : xmppConnectionService.getStories()) {
|
||||
if (story.getUuid().equals(currentStoryId) && story.getPublished() >= twentyFourHoursAgo) {
|
||||
publishedTimestamp = story.getPublished();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (publishedTimestamp > 0) {
|
||||
toolbarSubtitle.setText(DateUtils.getRelativeTimeSpanString(publishedTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS));
|
||||
} else {
|
||||
toolbarSubtitle.setText("");
|
||||
}
|
||||
|
||||
titleView.setText(titles.get(position));
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
appBarLayout.animate().alpha(0f).setDuration(200);
|
||||
bottomPanel.animate().alpha(0f).setDuration(200);
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void showSystemUi() {
|
||||
appBarLayout.animate().alpha(1f).setDuration(200);
|
||||
bottomPanel.animate().alpha(1f).setDuration(200);
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
updateUiForPosition(viewPager.getCurrentItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_story_view, menu);
|
||||
MenuItem deleteButton = menu.findItem(R.id.action_delete_story);
|
||||
if (contact != null && xmppConnectionService != null) {
|
||||
final Account storyOwner = xmppConnectionService.findAccountByJid(contact);
|
||||
if (storyOwner != null && storyOwner.isOnlineAndConnected()) {
|
||||
deleteButton.setVisible(true);
|
||||
this.mAccount = storyOwner;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_delete_story) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.delete_story_dialog_title)
|
||||
.setMessage(R.string.delete_story_dialog_message)
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
xmppConnectionService.retractStory(mAccount, storyIds.get(viewPager.getCurrentItem()), new UiCallback<Void>() {
|
||||
@Override
|
||||
public void success(Void aVoid) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(StoryViewActivity.this, R.string.story_deleted, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, Void object) {
|
||||
runOnUiThread(() -> Toast.makeText(StoryViewActivity.this, errorCode, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(android.app.PendingIntent pi, Void object) {
|
||||
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create()
|
||||
.show();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_reply_to_story) {
|
||||
// Pause the story progress before showing the dialog
|
||||
Fragment currentFragment = getSupportFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem());
|
||||
if (currentFragment instanceof StoryFragment) {
|
||||
((StoryFragment) currentFragment).pauseStory();
|
||||
}
|
||||
|
||||
int currentPos = viewPager.getCurrentItem();
|
||||
if (mAccount != null) {
|
||||
final LinearLayout container = new LinearLayout(this);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
int margin = (int) (16 * getResources().getDisplayMetrics().density);
|
||||
params.setMargins(margin, 0, margin, 0);
|
||||
final TextInputEditText input = new TextInputEditText(this);
|
||||
input.setLayoutParams(params);
|
||||
input.setHint(R.string.compose_message_hint);
|
||||
container.addView(input);
|
||||
|
||||
final AlertDialog dialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.reply_to_story)
|
||||
.setView(container)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.send, (d, which) -> {
|
||||
final String text = input.getText() != null ? input.getText().toString() : "";
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(mAccount, contact, false, false);
|
||||
String storyId = storyIds.get(currentPos);
|
||||
if (storyId == null) {
|
||||
return;
|
||||
}
|
||||
String storyUri = "xmpp:" + contact.asBareJid().toString() + "?;node=urn:xmpp:pubsub-social-feed:stories:0;item=" + storyId;
|
||||
final String messageBody = text.isEmpty() ? getString(R.string.reply_to_story) : text;
|
||||
|
||||
Message storyMessage = new Message(
|
||||
conversation,
|
||||
messageBody,
|
||||
conversation.getNextEncryption(),
|
||||
Message.STATUS_SEND
|
||||
);
|
||||
Element reference = new Element("reference", "urn:xmpp:reference:0");
|
||||
reference.setAttribute("type", "data");
|
||||
reference.setAttribute("uri", storyUri);
|
||||
storyMessage.addPayload(reference);
|
||||
storyMessage.setType(Message.TYPE_STORY);
|
||||
|
||||
Message.FileParams storyParams = new Message.FileParams();
|
||||
storyParams.url = storyUri;
|
||||
storyMessage.setFileParams(storyParams);
|
||||
|
||||
xmppConnectionService.sendMessage(storyMessage);
|
||||
switchToConversation(conversation);
|
||||
})
|
||||
.create();
|
||||
|
||||
dialog.setOnDismissListener(d -> {
|
||||
// Resume story progress when the dialog is dismissed
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem());
|
||||
if (fragment instanceof StoryFragment) {
|
||||
((StoryFragment) fragment).resumeStory();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private class StoryPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
public StoryPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
return StoryFragment.newInstance(urls.get(position), mimeTypes.get(position), urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return urls.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBackendConnected() {
|
||||
String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT);
|
||||
if (accountUuid != null) {
|
||||
mAccount = xmppConnectionService.findAccountByUuid(accountUuid);
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pauseStory() {
|
||||
hideSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeStory() {
|
||||
showSystemUi();
|
||||
}
|
||||
}
|
||||
|
|
@ -185,10 +185,6 @@ public class AccountAdapter extends RecyclerView.Adapter<AccountAdapter.ViewHold
|
|||
}
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
|
||||
// Notify that data has changed so we can persist
|
||||
if (mOnAccountMovedListener != null) {
|
||||
mOnAccountMovedListener.onAccountMoved();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.ui.widget.AvatarView;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
|
||||
public class CallsAdapter extends RecyclerView.Adapter<CallsAdapter.CallViewHolder> {
|
||||
|
||||
private final List<Message> calls;
|
||||
private final OnCallAgainClickListener callAgainClickListener;
|
||||
private final OnContactClickListener contactClickListener;
|
||||
private final XmppConnectionService xmppConnectionService;
|
||||
|
||||
public interface OnCallAgainClickListener {
|
||||
void onCallAgainClick(Message call, boolean isVideoCall);
|
||||
}
|
||||
|
||||
public interface OnContactClickListener {
|
||||
void onContactClick(Contact contact);
|
||||
}
|
||||
|
||||
public CallsAdapter(List<Message> calls, OnCallAgainClickListener callAgainClickListener, OnContactClickListener contactClickListener, XmppConnectionService xmppConnectionService) {
|
||||
this.calls = calls;
|
||||
this.callAgainClickListener = callAgainClickListener;
|
||||
this.contactClickListener = contactClickListener;
|
||||
this.xmppConnectionService = xmppConnectionService;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CallViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_call, parent, false);
|
||||
return new CallViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CallViewHolder holder, int position) {
|
||||
Message call = calls.get(position);
|
||||
holder.bind(call, callAgainClickListener, contactClickListener, xmppConnectionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return calls.size();
|
||||
}
|
||||
|
||||
static class CallViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarView avatar;
|
||||
private final TextView contactName;
|
||||
private final TextView callInfo;
|
||||
private final TextView callDate;
|
||||
private final ImageButton callAgainButton;
|
||||
|
||||
public CallViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
avatar = itemView.findViewById(R.id.avatar);
|
||||
contactName = itemView.findViewById(R.id.contact_name);
|
||||
callInfo = itemView.findViewById(R.id.call_info);
|
||||
callDate = itemView.findViewById(R.id.call_date);
|
||||
callAgainButton = itemView.findViewById(R.id.call_again);
|
||||
}
|
||||
|
||||
public void bind(final Message call, final OnCallAgainClickListener callAgainClickListener, final OnContactClickListener contactClickListener, final XmppConnectionService xmppConnectionService) {
|
||||
AvatarWorkerTask.loadAvatar(call.getConversation().getContact(), avatar, R.dimen.bubble_avatar_size);
|
||||
contactName.setText(call.getConversation().getContact().getDisplayName());
|
||||
|
||||
final Contact contact = call.getConversation().getContact();
|
||||
if (contact != null && !contact.isSelf()) {
|
||||
View.OnClickListener clickListener = v -> contactClickListener.onContactClick(contact);
|
||||
avatar.setOnClickListener(clickListener);
|
||||
contactName.setOnClickListener(clickListener);
|
||||
} else {
|
||||
avatar.setOnClickListener(null);
|
||||
contactName.setOnClickListener(null);
|
||||
}
|
||||
|
||||
final eu.siacs.conversations.entities.RtpSessionStatus rtpSessionStatus = eu.siacs.conversations.entities.RtpSessionStatus.of(call.getBody());
|
||||
final boolean received = call.getStatus() == Message.STATUS_RECEIVED;
|
||||
final boolean missed = received && !rtpSessionStatus.successful;
|
||||
|
||||
int color;
|
||||
if (missed) {
|
||||
color = ContextCompat.getColor(itemView.getContext(), R.color.red_700);
|
||||
} else {
|
||||
color = contactName.getCurrentTextColor();
|
||||
}
|
||||
callInfo.setTextColor(color);
|
||||
android.graphics.drawable.Drawable drawable = ContextCompat.getDrawable(itemView.getContext(), eu.siacs.conversations.entities.RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
|
||||
if (drawable != null) {
|
||||
drawable.setTint(color);
|
||||
final int size = (int) (18 * itemView.getContext().getResources().getDisplayMetrics().density);
|
||||
drawable.setBounds(0, 0, size, size);
|
||||
}
|
||||
|
||||
if (xmppConnectionService != null) callInfo.setText(UIHelper.getMessagePreview(xmppConnectionService, call).first);
|
||||
callInfo.setCompoundDrawables(drawable, null, null, null);
|
||||
callInfo.setCompoundDrawablePadding((int) (6 * itemView.getContext().getResources().getDisplayMetrics().density));
|
||||
|
||||
callDate.setText(UIHelper.readableTimeDifference(itemView.getContext(), call.getTimeSent(), false));
|
||||
callAgainButton.setOnClickListener(v -> {
|
||||
PopupMenu popup = new PopupMenu(v.getContext(), v);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
popup.setForceShowIcon(true);
|
||||
}
|
||||
popup.getMenuInflater().inflate(R.menu.call_again_context, popup.getMenu());
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.action_voice_call) {
|
||||
callAgainClickListener.onCallAgainClick(call, false);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_video_call) {
|
||||
callAgainClickListener.onCallAgainClick(call, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
popup.show();
|
||||
});
|
||||
TextView accountInfo = itemView.findViewById(R.id.account_info);
|
||||
if (xmppConnectionService != null && xmppConnectionService.getBooleanPreference("show_own_accounts", R.bool.show_own_accounts)) {
|
||||
accountInfo.setText(call.getConversation().getAccount().getJid().asBareJid().toString());
|
||||
accountInfo.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
accountInfo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ItemCommentBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Comment;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.XmppActivity;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.utils.AccountUtils;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class CommentsAdapter extends RecyclerView.Adapter<CommentsAdapter.CommentViewHolder> {
|
||||
|
||||
private final Post mPost;
|
||||
private final List<Comment> comments;
|
||||
private final XmppActivity mActivity;
|
||||
|
||||
private XmppConnectionService.OnCommentReceived mOnCommentReceived;
|
||||
|
||||
public CommentsAdapter(XmppActivity activity, Post post, List<Comment> comments) {
|
||||
this.mActivity = activity;
|
||||
this.mPost = post;
|
||||
this.comments = comments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView);
|
||||
if (mActivity.xmppConnectionService != null) {
|
||||
this.mOnCommentReceived = (postUuid, comment) -> {
|
||||
if (mPost.getId().equals(postUuid)) {
|
||||
mActivity.runOnUiThread(() -> {
|
||||
boolean found = false;
|
||||
for (Comment c : comments) {
|
||||
if (c.getId().equals(comment.getId())) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
comments.add(comment);
|
||||
Collections.sort(comments, (c1, c2) -> {
|
||||
Date d1 = c1.getPublished();
|
||||
Date d2 = c2.getPublished();
|
||||
if (d1 == null && d2 == null) return 0;
|
||||
if (d1 == null) return -1;
|
||||
if (d2 == null) return 1;
|
||||
return d1.compareTo(d2);
|
||||
});
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
mActivity.xmppConnectionService.addOnCommentReceivedListener(this.mOnCommentReceived);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
if (mActivity.xmppConnectionService != null && this.mOnCommentReceived != null) {
|
||||
mActivity.xmppConnectionService.removeOnCommentReceivedListener(this.mOnCommentReceived);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CommentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new CommentViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_comment, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CommentViewHolder holder, int position) {
|
||||
holder.bind(comments.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return comments.size();
|
||||
}
|
||||
|
||||
class CommentViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ItemCommentBinding binding;
|
||||
|
||||
CommentViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
binding = ItemCommentBinding.bind(itemView);
|
||||
}
|
||||
|
||||
void bind(Comment comment) {
|
||||
if (comment.getAuthor() != null) {
|
||||
final Jid authorJid = comment.getAuthor();
|
||||
if (mActivity.xmppConnectionService != null) {
|
||||
|
||||
final Account postAuthorAccount = mActivity.xmppConnectionService.findAccountByJid(mPost.getAuthor());
|
||||
Account contextAccount = postAuthorAccount != null ? postAuthorAccount : AccountUtils.getFirstEnabled(mActivity.xmppConnectionService.getAccounts());
|
||||
|
||||
if (contextAccount != null) {
|
||||
if (authorJid.asBareJid().equals(contextAccount.getJid().asBareJid())) {
|
||||
if (contextAccount.getDisplayName() == null) {
|
||||
binding.commentAuthorName.setText(authorJid.asBareJid().toString());
|
||||
} else {
|
||||
binding.commentAuthorName.setText(contextAccount.getDisplayName());
|
||||
}
|
||||
final Account self = contextAccount;
|
||||
binding.commentAuthorAvatar.setOnClickListener(v -> mActivity.switchToAccount(self));
|
||||
binding.commentAuthorName.setOnClickListener(v -> mActivity.switchToAccount(self));
|
||||
AvatarWorkerTask.loadAvatar(contextAccount, binding.commentAuthorAvatar, R.dimen.posts_comments_avatar_size);
|
||||
} else {
|
||||
Contact contact = contextAccount.getRoster().getContact(authorJid);
|
||||
if (contact != null) {
|
||||
if (contact.getDisplayName() == null) {
|
||||
binding.commentAuthorName.setText(contact.getJid().asBareJid().toString());
|
||||
} else {
|
||||
binding.commentAuthorName.setText(contact.getDisplayName());
|
||||
}
|
||||
binding.commentAuthorAvatar.setOnClickListener(v -> mActivity.switchToContactDetails(contact));
|
||||
binding.commentAuthorName.setOnClickListener(v -> mActivity.switchToContactDetails(contact));
|
||||
AvatarWorkerTask.loadAvatar(contact, binding.commentAuthorAvatar, R.dimen.posts_comments_avatar_size);
|
||||
} else {
|
||||
binding.commentAuthorName.setText(authorJid.asBareJid().toString());
|
||||
binding.commentAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
final Account commentAuthorAccount = mActivity.xmppConnectionService.findAccountByJid(comment.getAuthor());
|
||||
boolean iAmPostAuthor = postAuthorAccount != null && postAuthorAccount.isOnlineAndConnected();
|
||||
boolean iAmCommentAuthor = commentAuthorAccount != null && commentAuthorAccount.isOnlineAndConnected();
|
||||
|
||||
if (iAmPostAuthor || iAmCommentAuthor) {
|
||||
binding.retractButton.setVisibility(View.VISIBLE);
|
||||
|
||||
final Account accountToSendFrom = iAmCommentAuthor ? commentAuthorAccount : postAuthorAccount;
|
||||
binding.retractButton.setOnClickListener(v -> {
|
||||
new MaterialAlertDialogBuilder(mActivity)
|
||||
.setTitle(R.string.retract_comment)
|
||||
.setMessage(R.string.retract_comment_confirm)
|
||||
.setPositiveButton(R.string.retract, (dialog, which) -> retractComment(accountToSendFrom, comment))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
});
|
||||
} else {
|
||||
binding.retractButton.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.commentAuthorName.setText(authorJid.asBareJid().toString());
|
||||
binding.commentAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.commentAuthorName.setText(null);
|
||||
binding.commentAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
}
|
||||
binding.commentContent.setText(comment.getTitle());
|
||||
if (comment.getPublished() != null) {
|
||||
binding.commentTimestamp.setText(DateFormat.getDateTimeInstance().format(comment.getPublished()));
|
||||
} else {
|
||||
binding.commentTimestamp.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void retractComment(Account account, Comment comment) {
|
||||
if (mActivity.xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final XmppUri uri = new XmppUri(mPost.getCommentsNode());
|
||||
final Jid jid = uri.getJid();
|
||||
final String node = uri.getParameter("node");
|
||||
|
||||
mActivity.xmppConnectionService.retractPost(account, jid, node, comment.getId(), new XmppConnectionService.OnPostRetracted() {
|
||||
@Override
|
||||
public void onPostRetracted(String postId) {
|
||||
mActivity.runOnUiThread(() -> {
|
||||
int pos = getAdapterPosition();
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
comments.remove(pos);
|
||||
notifyItemRemoved(pos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostRetractionFailed() {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_retracting_comment, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "error retracting comment", e);
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_retracting_comment, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.PostsActivity;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||
|
||||
public class FollowSuggestionAdapter extends RecyclerView.Adapter<FollowSuggestionAdapter.ViewHolder> {
|
||||
|
||||
private final PostsActivity mPostsActivity;
|
||||
private final List<Contact> mContacts;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
|
||||
public FollowSuggestionAdapter(PostsActivity activity, XmppConnectionService service, List<Contact> contacts) {
|
||||
this.mPostsActivity = activity;
|
||||
this.mXmppConnectionService = service;
|
||||
this.mContacts = contacts;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_follow_suggestion, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final Contact contact = mContacts.get(position);
|
||||
holder.mContactName.setText(contact.getDisplayName());
|
||||
AvatarWorkerTask.loadAvatar(contact, holder.mAvatar, R.dimen.feed_suggestions_avatar_size);
|
||||
holder.mAvatar.setOnClickListener(v -> mPostsActivity.switchToContactDetails(contact));
|
||||
holder.mFollowButton.setOnClickListener(v -> {
|
||||
final Account account = contact.getAccount();
|
||||
mXmppConnectionService.subscribeTo(account, contact.getJid(), Namespace.MICROBLOG, packet -> {
|
||||
mPostsActivity.runOnUiThread(() -> {
|
||||
if (packet.getType() == Iq.Type.RESULT) {
|
||||
contact.setFollowed(true);
|
||||
mXmppConnectionService.updateContact(contact);
|
||||
mContacts.remove(contact);
|
||||
notifyDataSetChanged();
|
||||
mPostsActivity.loadPosts();
|
||||
} else {
|
||||
Toast.makeText(mPostsActivity, R.string.error_subscribing_to_feed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mContacts.size();
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final ImageView mAvatar;
|
||||
final TextView mContactName;
|
||||
final Button mFollowButton;
|
||||
|
||||
ViewHolder(View view) {
|
||||
super(view);
|
||||
mAvatar = view.findViewById(R.id.contact_avatar);
|
||||
mContactName = view.findViewById(R.id.contact_name);
|
||||
mFollowButton = view.findViewById(R.id.follow_button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
|
|||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.widget.ImageViewCompat;
|
||||
import androidx.customview.widget.ViewDragHelper;
|
||||
|
|
@ -91,7 +92,10 @@ import com.google.common.collect.ImmutableSet;
|
|||
import com.lelloman.identicon.view.GithubIdenticonView;
|
||||
|
||||
import de.monocles.chat.ui.DraggableListView;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.AddReactionActivity;
|
||||
import eu.siacs.conversations.ui.StoryViewActivity;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -99,6 +103,8 @@ import java.lang.ref.WeakReference;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -574,6 +580,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
BubbleMessageItemViewHolder viewHolder,
|
||||
CharSequence text,
|
||||
final BubbleColor bubbleColor) {
|
||||
viewHolder.storyPreview().setVisibility(View.GONE);
|
||||
viewHolder.downloadButton().setVisibility(View.GONE);
|
||||
viewHolder.audioPlayer().setVisibility(View.GONE);
|
||||
viewHolder.image().setVisibility(View.GONE);
|
||||
|
|
@ -593,6 +600,52 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
viewHolder.messageBody().setTextIsSelectable(false);
|
||||
}
|
||||
|
||||
private void displayPubSubMessage(
|
||||
final BubbleMessageItemViewHolder viewHolder,
|
||||
final Message message,
|
||||
final BubbleColor bubbleColor) {
|
||||
|
||||
// First, handle the text part of the message.
|
||||
// This ensures the text is always displayed for a story reply.
|
||||
displayTextMessage(viewHolder, message, bubbleColor);
|
||||
|
||||
// Now, find and display the story preview.
|
||||
final Pair<Jid, String> storyReference = message.getStoryReference();
|
||||
Story story = null;
|
||||
if (storyReference != null) {
|
||||
final XmppConnectionService xmppService = activity.xmppConnectionService;
|
||||
if (xmppService != null) {
|
||||
for (final Story s : activity.xmppConnectionService.getStories()) {
|
||||
if (s.getUuid().equals(storyReference.second)) {
|
||||
story = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only show the preview if a valid story was actually found.
|
||||
if (story != null) {
|
||||
viewHolder.storyPreview().setVisibility(View.VISIBLE);
|
||||
viewHolder.storyTitle().setText(story.getTitle());
|
||||
Glide.with(activity).load(story.getUrl()).into(viewHolder.storyThumbnail());
|
||||
final Story finalStory = story;
|
||||
viewHolder.storyPreview().setOnClickListener(v -> {
|
||||
final Intent intent = new Intent(activity, StoryViewActivity.class);
|
||||
intent.putExtra(StoryViewActivity.EXTRA_URLS, new ArrayList<>(Collections.singletonList(finalStory.getUrl())));
|
||||
intent.putExtra(StoryViewActivity.EXTRA_TITLES, new ArrayList<>(Collections.singletonList(finalStory.getTitle())));
|
||||
intent.putExtra(StoryViewActivity.EXTRA_STORY_IDS, new ArrayList<>(Collections.singletonList(finalStory.getUuid())));
|
||||
intent.putExtra(StoryViewActivity.EXTRA_MIME_TYPES, new ArrayList<>(Collections.singletonList(finalStory.getType())));
|
||||
intent.putExtra(StoryViewActivity.EXTRA_CONTACT, finalStory.getContact().asBareJid().toString());
|
||||
intent.putExtra(StoryViewActivity.EXTRA_ACCOUNT, message.getConversation().getAccount().getUuid());
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
} else {
|
||||
// If no story is found, we MUST hide the preview to prevent recycling issues.
|
||||
viewHolder.storyPreview().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayEmojiMessage(
|
||||
final BubbleMessageItemViewHolder viewHolder,
|
||||
final Message message,
|
||||
|
|
@ -737,6 +790,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
final BubbleMessageItemViewHolder viewHolder,
|
||||
final Message message,
|
||||
final BubbleColor bubbleColor) {
|
||||
if (message.getType() != Message.TYPE_STORY)
|
||||
viewHolder.storyPreview().setVisibility(View.GONE);
|
||||
viewHolder.inReplyToQuote().setVisibility(GONE);
|
||||
viewHolder.downloadButton().setVisibility(GONE);
|
||||
viewHolder.image().setVisibility(GONE);
|
||||
|
|
@ -1517,6 +1572,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
final int position,
|
||||
final Message message,
|
||||
final BubbleMessageItemViewHolder viewHolder) {
|
||||
viewHolder.storyPreview().setVisibility(View.GONE); //reset view state
|
||||
final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
|
||||
final boolean isInValidSession =
|
||||
message.isValidInSession() && (!omemoEncryption || message.isTrusted());
|
||||
|
|
@ -1638,7 +1694,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
}
|
||||
});
|
||||
|
||||
boolean footerWrap = false;
|
||||
final Transferable transferable = message.getTransferable();
|
||||
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
|
||||
|
||||
|
|
@ -1646,6 +1701,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
if (muted) {
|
||||
// Muted MUC participant
|
||||
displayInfoMessage(viewHolder, "Muted", bubbleColor);
|
||||
} else if (message.getType() == Message.TYPE_STORY || message.getStoryReference() != null) {
|
||||
displayPubSubMessage(viewHolder, message, bubbleColor);
|
||||
} else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
|
||||
if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
|
||||
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor);
|
||||
|
|
@ -2643,6 +2700,9 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
protected abstract TextView username();
|
||||
|
||||
protected abstract TextView showMore();
|
||||
protected abstract LinearLayout storyPreview();
|
||||
protected abstract ShapeableImageView storyThumbnail();
|
||||
protected abstract TextView storyTitle();
|
||||
}
|
||||
|
||||
private static class StartBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder {
|
||||
|
|
@ -2771,6 +2831,21 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
protected TextView showMore() {
|
||||
return this.binding.messageContent.showMore;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinearLayout storyPreview() {
|
||||
return this.binding.messageContent.storyPreview;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ShapeableImageView storyThumbnail() {
|
||||
return this.binding.messageContent.storyThumbnail;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TextView storyTitle() {
|
||||
return this.binding.messageContent.storyTitle;
|
||||
}
|
||||
}
|
||||
|
||||
private static class EndBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder {
|
||||
|
|
@ -2797,6 +2872,21 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
|
|||
return this.binding.messageContent.showMore;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinearLayout storyPreview() {
|
||||
return this.binding.messageContent.storyPreview;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ShapeableImageView storyThumbnail() {
|
||||
return this.binding.messageContent.storyThumbnail;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TextView storyTitle() {
|
||||
return this.binding.messageContent.storyTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImageView indicatorEdit() {
|
||||
return this.binding.editIndicator;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,673 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.MediaController;
|
||||
import android.widget.Toast;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ItemPostBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Comment;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Post;
|
||||
import eu.siacs.conversations.entities.StubConversation;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.CreatePostActivity;
|
||||
import eu.siacs.conversations.ui.OnSearchPerformed;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
import eu.siacs.conversations.ui.XmppActivity;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.utils.AccountUtils;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xml.XmlReader;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import io.noties.markwon.Markwon;
|
||||
|
||||
public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHolder> {
|
||||
|
||||
private final List<Post> posts;
|
||||
private final XmppActivity mActivity;
|
||||
private final Set<Post> expandedPosts = new HashSet<>();
|
||||
private final ActivityResultLauncher<Intent> postResultLauncher;
|
||||
private final Markwon markwon;
|
||||
private final OnSearchPerformed mOnSearchPerformed;
|
||||
|
||||
public PostsAdapter(XmppActivity activity, List<Post> posts, ActivityResultLauncher<Intent> launcher, OnSearchPerformed onSearchPerformed) {
|
||||
this.mActivity = activity;
|
||||
this.posts = posts;
|
||||
this.postResultLauncher = launcher;
|
||||
this.markwon = Markwon.create(activity);
|
||||
this.mOnSearchPerformed = onSearchPerformed;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PostViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new PostViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_post, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PostViewHolder holder, int position) {
|
||||
holder.bind(posts.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return posts.size();
|
||||
}
|
||||
|
||||
private void showImagePreviewDialog(String url) {
|
||||
if (mActivity == null || url == null) {
|
||||
return;
|
||||
}
|
||||
final Dialog dialog = new Dialog(mActivity);
|
||||
dialog.setContentView(R.layout.dialog_image_preview);
|
||||
ImageView imageView = dialog.findViewById(R.id.image_view);
|
||||
Glide.with(mActivity).load(url).into(imageView);
|
||||
imageView.setOnClickListener(v -> dialog.dismiss());
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showVideoPreviewDialog (String url) {
|
||||
if (mActivity == null || url == null) {
|
||||
return;
|
||||
}
|
||||
final Dialog dialog = new Dialog(mActivity);
|
||||
dialog.setContentView(R.layout.dialog_video_preview);
|
||||
VideoView videoView = dialog.findViewById(R.id.video_view);
|
||||
android.widget.FrameLayout frameLayout = dialog.findViewById(R.id.video_frame);
|
||||
Glide.with(mActivity)
|
||||
.asFile()
|
||||
.load(url)
|
||||
.into(new CustomTarget<File>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> transition) {
|
||||
videoView.setVideoURI(Uri.fromFile(resource));
|
||||
videoView.setOnPreparedListener(mp -> {
|
||||
mp.setLooping(true);
|
||||
videoView.start();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
Toast.makeText(mActivity, R.string.download_failed_file_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
MediaController controller = new MediaController(mActivity);
|
||||
controller.setAnchorView(frameLayout);
|
||||
videoView.setMediaController(controller);
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
class PostViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ItemPostBinding binding;
|
||||
|
||||
PostViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
binding = ItemPostBinding.bind(itemView);
|
||||
}
|
||||
|
||||
void bind(Post post) {
|
||||
binding.commentsList.setVisibility(View.GONE);
|
||||
binding.commentsList.setAdapter(null);
|
||||
binding.likeButton.setOnClickListener(null);
|
||||
binding.likeCount.setText("");
|
||||
binding.likeButton.setEnabled(false);
|
||||
binding.likeButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.favorite_border_24, 0, 0, 0);
|
||||
|
||||
final boolean isExpanded = expandedPosts.contains(post);
|
||||
|
||||
binding.postContentSummary.setVisibility(isExpanded ? View.GONE : View.VISIBLE);
|
||||
binding.postContentFull.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
|
||||
binding.postActions.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isExpanded) {
|
||||
loadCommentsAndLikes(post, this);
|
||||
}
|
||||
|
||||
final View.OnClickListener expandClickListener = v -> {
|
||||
if (expandedPosts.contains(post)) {
|
||||
expandedPosts.remove(post);
|
||||
} else {
|
||||
expandedPosts.add(post);}
|
||||
notifyItemChanged(getAdapterPosition());
|
||||
};
|
||||
|
||||
setupPostView(post, expandClickListener);
|
||||
}
|
||||
|
||||
private void setupPostView(final Post post, final View.OnClickListener expandClickListener) {
|
||||
final boolean isExpanded = expandedPosts.contains(post);
|
||||
final boolean hasAttachment = post.getAttachmentUrl() != null;
|
||||
final boolean isImage = hasAttachment && post.getAttachmentType() != null && post.getAttachmentType().startsWith("image/");
|
||||
final boolean isVideo = hasAttachment && post.getAttachmentType() != null && post.getAttachmentType().startsWith("video/");
|
||||
|
||||
binding.attachmentHint.setVisibility(hasAttachment && !isExpanded ? View.VISIBLE : View.GONE);
|
||||
binding.postImage.setVisibility(isExpanded && (isImage || isVideo) ? View.VISIBLE : View.GONE);
|
||||
binding.videoOverlayIcon.setVisibility(isExpanded && isVideo ? View.VISIBLE : View.GONE);
|
||||
binding.downloadButton.setVisibility(isExpanded && hasAttachment && !isImage && !isVideo ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isExpanded && (isImage || isVideo)) {
|
||||
binding.attachmentProgress.setVisibility(View.VISIBLE);
|
||||
Glide.with(mActivity)
|
||||
.load(post.getAttachmentUrl()) .listener(new com.bumptech.glide.request.RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, @Nullable Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, boolean isFirstResource) {
|
||||
binding.attachmentProgress.setVisibility(View.GONE);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, @NonNull com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
|
||||
binding.attachmentProgress.setVisibility(View.GONE);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(binding.postImage);binding.postImage.setOnClickListener(v -> {
|
||||
if (isImage) showImagePreviewDialog(post.getAttachmentUrl());
|
||||
else showVideoPreviewDialog(post.getAttachmentUrl());
|
||||
});
|
||||
}
|
||||
|
||||
final List<Account> postAccounts = new ArrayList<>();
|
||||
if (post.getAuthor() != null && mActivity.xmppConnectionService != null) {
|
||||
for (Account account : mActivity.xmppConnectionService.getAccounts()) {
|
||||
if (account.isOnlineAndConnected()) {
|
||||
if (account.getJid().asBareJid().equals(post.getAuthor().asBareJid()) || (account.getRoster() != null && account.getRoster().getContact(post.getAuthor().asBareJid()) != null)) {
|
||||
postAccounts.add(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mActivity.xmppConnectionService == null) {
|
||||
binding.editButton.setVisibility(View.GONE);
|
||||
binding.deleteButton.setVisibility(View.GONE);
|
||||
binding.replyButton.setVisibility(View.GONE);
|
||||
binding.commentButton.setVisibility(View.GONE);
|
||||
binding.downloadButton.setVisibility(View.GONE);
|
||||
binding.likeButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
final Account ownAccount = post.getAuthor() != null ? mActivity.xmppConnectionService.findAccountByJid(post.getAuthor().asBareJid()) : null;
|
||||
|
||||
binding.downloadButton.setOnClickListener(v -> {
|
||||
if (postAccounts.size() == 1) {
|
||||
downloadAttachment(postAccounts.get(0), post);
|
||||
} else { showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_download), postAccounts, account -> downloadAttachment(account, post));
|
||||
}
|
||||
});
|
||||
|
||||
if (post.getContent() != null) {
|
||||
final CharSequence markdown = markwon.toMarkdown(post.getContent());
|
||||
binding.postContentSummary.setText(markdown);
|
||||
binding.postContentSummary.setMovementMethod(null);
|
||||
binding.postContentSummary.setOnClickListener(expandClickListener);
|
||||
|
||||
final SpannableString spannable = new SpannableString(markdown);
|
||||
final Matcher matcher = Pattern.compile("#[\\p{L}\\p{N}]+").matcher(spannable);
|
||||
while (matcher.find()) {
|
||||
final String hashtag = matcher.group(0);
|
||||
spannable.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
mOnSearchPerformed.onSearchPerformed(hashtag);
|
||||
}
|
||||
}, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
binding.postContentFull.setText(spannable);
|
||||
binding.postContentFull.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
|
||||
binding.postContentFull.setOnClickListener(null);
|
||||
|
||||
} else {
|
||||
binding.postContentSummary.setText("");
|
||||
binding.postContentFull.setText("");
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(expandClickListener);
|
||||
binding.postTitle.setOnClickListener(expandClickListener);
|
||||
|
||||
binding.commentButton.setOnClickListener(v -> {
|
||||
if (post.getAuthor() != null) {
|
||||
if (postAccounts.size() == 1) {
|
||||
replyToPost(postAccounts.get(0), post);
|
||||
} else {
|
||||
showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_reply), postAccounts, account -> replyToPost(account, post));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
binding.replyButton.setVisibility(View.GONE);
|
||||
|
||||
binding.shareButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, post.getTitle() + "\n" + post.getContent());
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.share_post_with)));
|
||||
});
|
||||
|
||||
if (ownAccount != null && ownAccount.isOnlineAndConnected()) {
|
||||
binding.editButton.setVisibility(View.VISIBLE);
|
||||
binding.deleteButton.setVisibility(View.VISIBLE);
|
||||
binding.editButton.setOnClickListener(v -> {
|
||||
editPost(ownAccount, post);
|
||||
});
|
||||
binding.deleteButton.setOnClickListener(v -> {
|
||||
new MaterialAlertDialogBuilder(mActivity)
|
||||
.setTitle(R.string.retract_post)
|
||||
.setMessage(R.string.retract_post_confirm)
|
||||
.setPositiveButton(R.string.retract, (dialog, which) -> {
|
||||
mActivity.xmppConnectionService.retractPost(ownAccount, Namespace.MICROBLOG, post.getId(),
|
||||
new XmppConnectionService.OnPostRetracted() {
|
||||
|
||||
@Override
|
||||
public void onPostRetracted(String postId) {
|
||||
mActivity.runOnUiThread(() -> {
|
||||
mActivity.xmppConnectionService.databaseBackend.deletePost(postId);
|
||||
int pos = getAdapterPosition();
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
posts.remove(pos);
|
||||
notifyItemRemoved(pos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostRetractionFailed() {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_retract_post, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
});
|
||||
} else {
|
||||
binding.editButton.setVisibility(View.GONE);
|
||||
binding.deleteButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (post.getAuthor() != null) {
|
||||
final Jid authorJid = post.getAuthor();
|
||||
Account displayAccount = ownAccount;
|
||||
if (displayAccount == null && postAccounts.size() > 0) {
|
||||
displayAccount = postAccounts.get(0);
|
||||
}
|
||||
if (displayAccount == null) {
|
||||
displayAccount = AccountUtils.getFirstEnabled(mActivity.xmppConnectionService.getAccounts());
|
||||
}
|
||||
|
||||
if (displayAccount != null) {
|
||||
if (authorJid.asBareJid().equals(displayAccount.getJid().asBareJid())) {
|
||||
final String displayName = displayAccount.getDisplayName();
|
||||
binding.postAuthorName.setText(displayName != null && !displayName.isEmpty() ? displayName : displayAccount.getJid().asBareJid().toString());
|
||||
AvatarWorkerTask.loadAvatar(displayAccount, binding.postAuthorAvatar, R.dimen.bubble_avatar_size);
|
||||
final Account self = displayAccount;
|
||||
binding.postAuthorAvatar.setOnClickListener(v -> mActivity.switchToAccount(self));
|
||||
binding.postAuthorName.setOnClickListener(v -> mActivity.switchToAccount(self));
|
||||
} else {
|
||||
Contact contact = displayAccount.getRoster().getContact(authorJid);
|
||||
if (contact != null) {
|
||||
final String displayName = contact.getDisplayName();
|
||||
binding.postAuthorName.setText(displayName != null && !displayName.isEmpty() ? displayName : contact.getJid().asBareJid().toString());
|
||||
AvatarWorkerTask.loadAvatar(contact, binding.postAuthorAvatar, R.dimen.bubble_avatar_size);
|
||||
binding.postAuthorAvatar.setOnClickListener(v -> mActivity.switchToContactDetails(contact));
|
||||
binding.postAuthorName.setOnClickListener(v -> mActivity.switchToContactDetails(contact));
|
||||
} else {
|
||||
binding.postAuthorName.setText(authorJid.asBareJid().toString());
|
||||
binding.postAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
binding.postAuthorAvatar.setOnClickListener(null);
|
||||
binding.postAuthorName.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.postAuthorName.setText(authorJid.asBareJid().toString());
|
||||
binding.postAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
binding.postAuthorAvatar.setOnClickListener(null);
|
||||
binding.postAuthorName.setOnClickListener(null);
|
||||
}
|
||||
} else {
|
||||
binding.postAuthorName.setText(null);
|
||||
binding.postAuthorAvatar.setImageResource(R.drawable.ic_person_24dp);
|
||||
binding.postAuthorAvatar.setOnClickListener(null);
|
||||
binding.postAuthorName.setOnClickListener(null);
|
||||
}
|
||||
binding.postTitle.setText(post.getTitle());
|
||||
if (post.getPublished() != null) {
|
||||
binding.postTimestamp.setText(DateFormat.getDateTimeInstance().format(post.getPublished()));
|
||||
} else {
|
||||
binding.postTimestamp.setText(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCommentsAndLikes(final Post post, final PostViewHolder holder) {
|
||||
if (mActivity.xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final XmppUri uri = new XmppUri(post.getCommentsNode());
|
||||
final Jid jid = uri.getJid();
|
||||
final String node = uri.getParameter("node");
|
||||
if (jid != null && node != null) {
|
||||
mActivity.xmppConnectionService.fetchPubsubItems(jid, node, new XmppConnectionService.OnPubsubItemsFetched() {
|
||||
@Override
|
||||
public void onPubsubItemsFetched(String feedXml) {
|
||||
try {
|
||||
final XmlReader reader = new XmlReader();
|
||||
reader.setInputStream(new java.io.ByteArrayInputStream(feedXml.getBytes()));
|
||||
final Element pubsub = reader.readElement(reader.readTag());
|
||||
final List<Comment> comments = new ArrayList<>();
|
||||
final List<Comment> likes = new ArrayList<>();
|
||||
if (pubsub != null) {
|
||||
final Element items = pubsub.findChild("items");
|
||||
if (items != null) {
|
||||
for (Element item : items.getChildren()) {
|
||||
if ("item".equals(item.getName())) {
|
||||
final Comment comment = Comment.fromElement(item);
|
||||
if (comment != null) {
|
||||
if ("♥".equals(comment.getTitle())) {
|
||||
likes.add(comment);
|
||||
} else {
|
||||
comments.add(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mActivity.runOnUiThread(() -> {
|
||||
if (holder.getAdapterPosition() == RecyclerView.NO_POSITION || posts.get(holder.getAdapterPosition()) != post) {
|
||||
return;
|
||||
}
|
||||
if (!comments.isEmpty()) {
|
||||
java.util.Collections.sort(comments, (c1, c2) -> {
|
||||
Date d1 = c1.getPublished();
|
||||
Date d2 = c2.getPublished();
|
||||
if (d1 == null && d2 == null) return 0;
|
||||
if (d1 == null) return -1;
|
||||
if (d2 == null) return 1;
|
||||
return d1.compareTo(d2);
|
||||
});
|
||||
CommentsAdapter commentsAdapter = new CommentsAdapter(mActivity, post, comments);
|
||||
holder.binding.commentsList.setLayoutManager(new LinearLayoutManager(mActivity));
|
||||
holder.binding.commentsList.setAdapter(commentsAdapter);
|
||||
holder.binding.commentsList.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.binding.commentsList.setVisibility(View.GONE);
|
||||
}
|
||||
// Update like button UI and logic
|
||||
setupLikeButton(post, holder, likes);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "error parsing comments", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPubsubItemsFetchFailed() {
|
||||
Log.e(Config.LOGTAG, "failed to fetch comments for post " + post.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(Config.LOGTAG, "error parsing comments node uri", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupLikeButton(final Post post, final PostViewHolder holder, final List<Comment> likes) {
|
||||
final List<Account> onlineAccounts = mActivity.xmppConnectionService.getAccounts().stream()
|
||||
.filter(Account::isOnlineAndConnected)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
holder.binding.likeButton.setEnabled(false);
|
||||
holder.binding.likeCount.setText(String.valueOf(likes.size()));
|
||||
return;
|
||||
}
|
||||
holder.binding.likeButton.setEnabled(true);
|
||||
holder.binding.likeCount.setText(String.valueOf(likes.size()));
|
||||
|
||||
holder.binding.likeButton.setOnLongClickListener(v -> {
|
||||
showLikesDialog(likes);
|
||||
return true;
|
||||
});
|
||||
|
||||
Comment myLike = null;
|
||||
Account myLikerAccount = null;
|
||||
for (Account acc : onlineAccounts) {
|
||||
for (Comment like : likes) {
|
||||
if (like.getAuthor() != null && like.getAuthor().asBareJid().equals(acc.getJid().asBareJid())) {
|
||||
myLike = like;
|
||||
myLikerAccount = acc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (myLike != null) break;
|
||||
}
|
||||
|
||||
final boolean hasLiked = myLike != null;
|
||||
holder.binding.likeButton.setCompoundDrawablesWithIntrinsicBounds(
|
||||
hasLiked ? R.drawable.favorite_filled_24 : R.drawable.favorite_border_24, 0, 0, 0);
|
||||
|
||||
final Comment finalMyLike = myLike;
|
||||
final Account finalMyLikerAccount = myLikerAccount;
|
||||
holder.binding.likeButton.setOnClickListener(v -> {
|
||||
if (hasLiked && finalMyLikerAccount != null) {
|
||||
retractLike(finalMyLikerAccount, finalMyLike, post, holder);
|
||||
} else {
|
||||
if (onlineAccounts.size() > 1) {
|
||||
showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_like), onlineAccounts, selectedAccount -> {
|
||||
publishLike(selectedAccount, post, holder);
|
||||
});
|
||||
} else {
|
||||
publishLike(onlineAccounts.get(0), post, holder);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showLikesDialog(List<Comment> likes) {
|
||||
if (likes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final ArrayList<String> likerDisplayNames = new ArrayList<>();
|
||||
final List<Account> accounts = mActivity.xmppConnectionService.getAccounts();
|
||||
for (Comment like : likes) {
|
||||
if (like.getAuthor() != null) {
|
||||
final Jid authorJid = like.getAuthor().asBareJid();
|
||||
String displayName = null;
|
||||
for (Account account : accounts) {
|
||||
if (account.getRoster() != null) {
|
||||
final Contact contact = account.getRoster().getContact(authorJid);
|
||||
if (contact != null && contact.getDisplayName() != null && !contact.getDisplayName().isEmpty()) {
|
||||
displayName = contact.getDisplayName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayName != null) {
|
||||
likerDisplayNames.add(displayName);
|
||||
} else {
|
||||
likerDisplayNames.add(authorJid.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_list_item_1, likerDisplayNames);
|
||||
|
||||
new MaterialAlertDialogBuilder(mActivity)
|
||||
.setTitle(mActivity.getResources().getQuantityString(R.plurals.liked_by_title, likes.size(), likes.size()))
|
||||
.setAdapter(adapter, null)
|
||||
.setPositiveButton(R.string.action_close, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void publishLike(final Account account, final Post post, final PostViewHolder holder) {
|
||||
mActivity.xmppConnectionService.publishComment(account, post.getCommentsNode(), "♥", new XmppConnectionService.OnPostPublished() {
|
||||
@Override
|
||||
public void onPostPublished() {
|
||||
mActivity.runOnUiThread(() -> loadCommentsAndLikes(post, holder));
|
||||
}
|
||||
@Override
|
||||
public void onPostPublishFailed() {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_liking_post, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void retractLike(final Account account, final Comment like, final Post post, final PostViewHolder holder) {
|
||||
try {
|
||||
final XmppUri uri = new XmppUri(post.getCommentsNode());
|
||||
final Jid jid = uri.getJid();
|
||||
final String node = uri.getParameter("node");
|
||||
mActivity.xmppConnectionService.retractPost(account, jid, node, like.getId(), new XmppConnectionService.OnPostRetracted() {
|
||||
@Override
|
||||
public void onPostRetracted(String postId) {
|
||||
mActivity.runOnUiThread(() -> loadCommentsAndLikes(post, holder));
|
||||
}
|
||||
@Override
|
||||
public void onPostRetractionFailed() {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_removing_like, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "error retracting like", e);
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_removing_like, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadAttachment(Account account, Post post) {
|
||||
if (mActivity.xmppConnectionService == null) {
|
||||
return;
|
||||
}
|
||||
final var message = new Message(new StubConversation(account, "", null, 0), null, Message.ENCRYPTION_NONE);
|
||||
message.setType(Message.TYPE_FILE);
|
||||
Message.FileParams params = new Message.FileParams();
|
||||
params.url = post.getAttachmentUrl();
|
||||
message.setFileParams(params);
|
||||
|
||||
mActivity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
|
||||
mActivity.xmppConnectionService.copyAttachmentToDownloadsFolder(message, new UiCallback<Integer>() {
|
||||
@Override
|
||||
public void success(Integer object) {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.save_to_downloads_success, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, Integer object) {
|
||||
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, errorCode, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequired(android.app.PendingIntent pi, Integer object) {
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
Toast.makeText(mActivity, R.string.download_started, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void showAccountSelectionDialog(String title, List<Account> accounts, java.util.function.Consumer<Account> onAccountSelected) {
|
||||
if (accounts.size() == 0) {
|
||||
Toast.makeText(mActivity, R.string.no_active_account, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (accounts.size() == 1) {
|
||||
onAccountSelected.accept(accounts.get(0));
|
||||
return;
|
||||
} final ArrayAdapter<String> adapter = new ArrayAdapter<>(mActivity, android.R.layout.simple_list_item_1);
|
||||
final java.util.Map<String, Account> accountMap = new java.util.HashMap<>();
|
||||
for (Account account : accounts) {
|
||||
String jid = account.getJid().asBareJid().toString();
|
||||
adapter.add(jid);
|
||||
accountMap.put(jid, account);
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(mActivity)
|
||||
.setTitle(title)
|
||||
.setAdapter(adapter, (dialog, which) -> {
|
||||
String jid = adapter.getItem(which);
|
||||
onAccountSelected.accept(accountMap.get(jid));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void replyToPost(Account account, Post post) {
|
||||
Intent intent = new Intent(mActivity, CreatePostActivity.class);
|
||||
intent.putExtra("in_reply_to_id", post.getId());
|
||||
intent.putExtra("in_reply_to_node", post.getCommentsNode());
|
||||
intent.putExtra("post_id", post.getId());
|
||||
intent.putExtra("account", account.getUuid());
|
||||
intent.putExtra("post_title", post.getTitle());
|
||||
intent.putExtra("post_content", post.getContent());
|
||||
postResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void editPost(Account account, Post post) {
|
||||
Intent intent = new Intent(mActivity, CreatePostActivity.class);
|
||||
intent.putExtra("post_id", post.getId());
|
||||
intent.putExtra("title", post.getTitle());
|
||||
intent.putExtra("content", post.getContent());
|
||||
intent.putExtra("account", account.getUuid());
|
||||
if (post.getAttachmentUrl() != null) {
|
||||
intent.putExtra("attachment_url", post.getAttachmentUrl());
|
||||
intent.putExtra("attachment_type", post.getAttachmentType());
|
||||
}
|
||||
postResultLauncher.launch(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.os.Handler;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Story;
|
||||
import eu.siacs.conversations.ui.StoryViewActivity;
|
||||
import eu.siacs.conversations.ui.XmppActivity;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class StoryAdapter extends RecyclerView.Adapter<StoryAdapter.StoryViewHolder> {
|
||||
|
||||
private final XmppActivity activity;
|
||||
private final List<Story> stories;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
private final Runnable refreshRunnable;
|
||||
private RecyclerView recyclerView;
|
||||
|
||||
public StoryAdapter(XmppActivity activity, List<Story> stories) {
|
||||
this.activity = activity;
|
||||
this.stories = stories;
|
||||
this.refreshRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (recyclerView != null) {
|
||||
// This is a more efficient way to refresh just the visible items
|
||||
final LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
final int first = layoutManager.findFirstVisibleItemPosition();
|
||||
final int last = layoutManager.findLastVisibleItemPosition();
|
||||
if (first != RecyclerView.NO_POSITION) {
|
||||
notifyItemRangeChanged(first, (last - first) + 1, "payload_time");
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 60000); // Run again in 1 minute
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public StoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_story, parent, false);
|
||||
return new StoryViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StoryViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.contains("payload_time")) {
|
||||
final Story story = stories.get(position);
|
||||
holder.storyTime.setText(DateUtils.getRelativeTimeSpanString(story.getPublished(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS));
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StoryViewHolder holder, int position) {
|
||||
final Story story = stories.get(position);
|
||||
final Jid jid = story.getContact();
|
||||
final int avatarSize = activity.getResources().getDimensionPixelSize(R.dimen.avatar_story_size);
|
||||
|
||||
Contact contact = null;
|
||||
Account storyAccount = null;
|
||||
|
||||
List<Contact> contacts = activity.xmppConnectionService.findContacts(jid, null);
|
||||
if (!contacts.isEmpty()) {
|
||||
Contact bestContact = null;
|
||||
int bestScore = -1;
|
||||
for (Contact c : contacts) {
|
||||
int score = 0;
|
||||
if (c.getAccount().getStatus() == Account.State.ONLINE) {
|
||||
score += 2;
|
||||
}
|
||||
Drawable d = activity.xmppConnectionService.getAvatarService().get(c, avatarSize);
|
||||
if (!(d instanceof eu.siacs.conversations.services.AvatarService.TextDrawable)) {
|
||||
score += 1;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestContact = c;
|
||||
}
|
||||
}
|
||||
contact = bestContact;
|
||||
}
|
||||
|
||||
if (contact != null) {
|
||||
storyAccount = contact.getAccount();
|
||||
} else if (activity.xmppConnectionService.findAccountByJid(jid) != null) {
|
||||
storyAccount = activity.xmppConnectionService.findAccountByJid(jid);
|
||||
if (storyAccount != null) {
|
||||
contact = storyAccount.getSelfContact();
|
||||
}
|
||||
}
|
||||
|
||||
if (contact != null) {
|
||||
holder.storyTitle.setText(contact.getDisplayName());
|
||||
Conversation conversation = activity.xmppConnectionService.findOrCreateConversation(storyAccount, contact.getJid(), false, false);
|
||||
AvatarWorkerTask.loadAvatar(conversation, holder.storyImage, R.dimen.avatar_on_conversation_overview);
|
||||
} else {
|
||||
holder.storyTitle.setText(jid.asBareJid().toString());
|
||||
holder.storyImage.setImageResource(R.drawable.ic_person_black_48dp);
|
||||
}
|
||||
|
||||
holder.storyTime.setText(DateUtils.getRelativeTimeSpanString(story.getPublished(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS));
|
||||
|
||||
Glide.with(activity).load(story.getUrl()).into(holder.storyPreview);
|
||||
|
||||
final Account finalStoryAccount = storyAccount;
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(activity, StoryViewActivity.class);
|
||||
ArrayList<String> urls = new ArrayList<>();
|
||||
ArrayList<String> titles = new ArrayList<>();
|
||||
ArrayList<String> storyIds = new ArrayList<>();
|
||||
ArrayList<String> mimeTypes = new ArrayList<>();
|
||||
for (Story s : activity.xmppConnectionService.getStories()) {
|
||||
if (s.getContact().asBareJid().equals(story.getContact().asBareJid())) {
|
||||
urls.add(s.getUrl());
|
||||
titles.add(s.getTitle());
|
||||
storyIds.add(s.getUuid());
|
||||
mimeTypes.add(s.getType());
|
||||
}
|
||||
}
|
||||
intent.putStringArrayListExtra(StoryViewActivity.EXTRA_URLS, urls);
|
||||
intent.putStringArrayListExtra(StoryViewActivity.EXTRA_TITLES, titles);
|
||||
intent.putStringArrayListExtra(StoryViewActivity.EXTRA_STORY_IDS, storyIds);
|
||||
intent.putStringArrayListExtra(StoryViewActivity.EXTRA_MIME_TYPES, mimeTypes);
|
||||
intent.putExtra(StoryViewActivity.EXTRA_CONTACT, story.getContact().asBareJid().toString());
|
||||
if (finalStoryAccount != null) {
|
||||
intent.putExtra(StoryViewActivity.EXTRA_ACCOUNT, finalStoryAccount.getUuid());
|
||||
}
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return stories.size();
|
||||
}
|
||||
|
||||
static class StoryViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final ImageView storyImage;
|
||||
final TextView storyTitle;
|
||||
final TextView storyTime;
|
||||
final ImageView storyPreview;
|
||||
|
||||
StoryViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
storyImage = itemView.findViewById(R.id.story_image);
|
||||
storyTitle = itemView.findViewById(R.id.story_title);
|
||||
storyTime = itemView.findViewById(R.id.story_time);
|
||||
storyPreview = itemView.findViewById(R.id.story_preview);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView);
|
||||
this.recyclerView = recyclerView;
|
||||
handler.post(refreshRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
this.recyclerView = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,14 +59,14 @@ public class ConnectionSettingsFragment extends XmppPreferenceFragment {
|
|||
|
||||
final var dnsv4Server = (EditTextPreference) findPreference("dns_server_ipv4");
|
||||
if (dnsv4Server != null) {
|
||||
dnsv4Server.setText("194.242.2.2");
|
||||
dnsv4Server.setText(null);
|
||||
}
|
||||
|
||||
final var dnsv6Server = (EditTextPreference) findPreference("dns_server_ipv6");
|
||||
if (dnsv6Server != null) {
|
||||
dnsv6Server.setText("[2a07:e340::2]");
|
||||
dnsv6Server.setText(null);
|
||||
}
|
||||
|
||||
reconnectAccounts();
|
||||
Toast.makeText(requireSettingsActivity(),R.string.dns_server_reset,Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
});
|
||||
|
|
@ -105,7 +105,7 @@ public class ConnectionSettingsFragment extends XmppPreferenceFragment {
|
|||
reconnectAccounts();
|
||||
requireService().reinitializeMuclumbusService();
|
||||
}
|
||||
case AppSettings.SHOW_CONNECTION_OPTIONS, AppSettings.PREFER_IPV6 -> {
|
||||
case AppSettings.SHOW_CONNECTION_OPTIONS, AppSettings.PREFER_IPV6, "dns_server_ipv4", "dns_server_ipv6" -> {
|
||||
reconnectAccounts();
|
||||
}
|
||||
}
|
||||
|
|
@ -146,4 +146,4 @@ public class ConnectionSettingsFragment extends XmppPreferenceFragment {
|
|||
"%s is not %s",
|
||||
activity.getClass().getName(), SettingsActivity.class.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package eu.siacs.conversations.ui.fragment.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
|
@ -15,8 +17,8 @@ import com.google.android.material.color.DynamicColors;
|
|||
import java.io.File;
|
||||
|
||||
import eu.siacs.conversations.AppSettings;
|
||||
import eu.siacs.conversations.Conversations;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.medialib.activities.EditActivity;
|
||||
import eu.siacs.conversations.ui.activity.SettingsActivity;
|
||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
||||
import eu.siacs.conversations.utils.ChatBackgroundHelper;
|
||||
|
|
@ -24,6 +26,8 @@ import eu.siacs.conversations.utils.ThemeHelper;
|
|||
|
||||
public class InterfaceSettingsFragment extends XmppPreferenceFragment {
|
||||
|
||||
private static final int REQUEST_EDIT_BACKGROUND = 9124;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
setPreferencesFromResource(R.xml.preferences_interface, rootKey);
|
||||
|
|
@ -145,12 +149,33 @@ public class InterfaceSettingsFragment extends XmppPreferenceFragment {
|
|||
activity.getClass().getName(), SettingsActivity.class.getName()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND) {
|
||||
final Uri imageUri = data == null ? null : data.getData();
|
||||
if (imageUri != null) {
|
||||
final Intent editIntent = new Intent(getActivity(), EditActivity.class);
|
||||
editIntent.setData(imageUri);
|
||||
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivityForResult(editIntent, REQUEST_EDIT_BACKGROUND);
|
||||
return;
|
||||
}
|
||||
} else if (requestCode == REQUEST_EDIT_BACKGROUND) {
|
||||
Uri uri = data != null ? (Uri) data.getParcelableExtra(EditActivity.KEY_EDITED_URI) : null;
|
||||
if (uri == null && data != null) {
|
||||
uri = data.getData();
|
||||
}
|
||||
|
||||
ChatBackgroundHelper.onActivityResult(requireSettingsActivity(), requestCode, resultCode, data, null);
|
||||
if (uri != null) {
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.setData(uri);
|
||||
ChatBackgroundHelper.onActivityResult(requireSettingsActivity(), ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND, resultCode, resultIntent, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -79,9 +79,6 @@ public class PrivacySettingsFragment extends XmppPreferenceFragment {
|
|||
requireService().toggleScreenEventReceiver();
|
||||
requireService().refreshAllPresences();
|
||||
}
|
||||
case AppSettings.CUSTOM_RESOURCE_NAME -> {
|
||||
reconnectAccounts();
|
||||
}
|
||||
case AppSettings.CONFIRM_MESSAGES,
|
||||
AppSettings.BROADCAST_LAST_ACTIVITY,
|
||||
AppSettings.ALLOW_MESSAGE_CORRECTION,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ public class QuoteHelper {
|
|||
}
|
||||
|
||||
public static boolean isNestedTooDeeply(CharSequence line) {
|
||||
return isNestedTooDeeply(line, Config.QUOTING_MAX_DEPTH);
|
||||
}
|
||||
|
||||
public static boolean isNestedTooDeeply(CharSequence line, int maxDepth) {
|
||||
if (isPositionQuoteStart(line, 0)) {
|
||||
int nestingDepth = 1;
|
||||
for (int i = 1; i < line.length(); i++) {
|
||||
|
|
@ -107,7 +111,7 @@ public class QuoteHelper {
|
|||
break;
|
||||
}
|
||||
}
|
||||
return nestingDepth >= (Config.QUOTING_MAX_DEPTH);
|
||||
return nestingDepth >= maxDepth;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,4 +132,18 @@ public class AccountUtils {
|
|||
manageAccounts.setVisible(MANAGE_ACCOUNT_ACTIVITY != null);
|
||||
}
|
||||
}
|
||||
|
||||
public static Account getFirstEnabled(List<Account> accounts) {
|
||||
for (Account account : accounts) {
|
||||
if (account.isOnlineAndConnected()) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
for (Account account : accounts) {
|
||||
if (account.isEnabled()) {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ package eu.siacs.conversations.utils;
|
|||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
|
|
@ -49,6 +50,10 @@ public class MessageUtils {
|
|||
public static final String EMPTY_STRING = "";
|
||||
|
||||
public static String prepareQuote(final Message message) {
|
||||
return prepareQuote(message, Config.QUOTING_MAX_DEPTH, -1);
|
||||
}
|
||||
|
||||
public static String prepareQuote(final Message message, int maxDepth, int maxLines) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
final String body;
|
||||
if (message.hasMeCommand()) {
|
||||
|
|
@ -66,14 +71,17 @@ public class MessageUtils {
|
|||
} else {
|
||||
body = message.getQuoteableBody();
|
||||
}
|
||||
int lines = 0;
|
||||
for (String line : body.split("\n")) {
|
||||
if (!(line.length() <= 0) && QuoteHelper.isNestedTooDeeply(line)) {
|
||||
continue;
|
||||
}
|
||||
if (maxLines > 0 && maxLines <= lines) break;
|
||||
if (builder.length() != 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
builder.append(line.trim());
|
||||
lines++;
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,8 @@ public class Resolver {
|
|||
final AbstractDnsClient dnssecclient = DnssecResolverApi.INSTANCE.getClient();
|
||||
if (dnssecclient instanceof ReliableDnsClient) {
|
||||
((ReliableDnsClient) dnssecclient).setUseHardcodedDnsServers(false);
|
||||
// If your DNS server sucks, just don't do DNSSEC
|
||||
((ReliableDnsClient) dnssecclient).setMode(ReliableDnsClient.Mode.recursiveOnly);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
package eu.siacs.conversations.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
|
@ -38,6 +39,7 @@ import eu.siacs.conversations.Config;
|
|||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Call;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
|
|
@ -60,7 +62,7 @@ import java.util.Locale;
|
|||
|
||||
public class UIHelper {
|
||||
|
||||
private static final List<String> LOCATION_QUESTIONS =
|
||||
private static final List<String> LOCATION_QUESTIONS =
|
||||
Arrays.asList(
|
||||
"where are you", // en
|
||||
"where are you now", // en
|
||||
|
|
@ -81,12 +83,12 @@ public class UIHelper {
|
|||
"donde estas" // es
|
||||
);
|
||||
|
||||
private static final List<Character> PUNCTIONATION =
|
||||
private static final List<Character> PUNCTIONATION =
|
||||
Arrays.asList('.', ',', '?', '!', ';', ':');
|
||||
|
||||
private static final int SHORT_DATE_FLAGS =
|
||||
private static final int SHORT_DATE_FLAGS =
|
||||
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
|
||||
private static final int FULL_DATE_FLAGS =
|
||||
private static final int FULL_DATE_FLAGS =
|
||||
DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
|
||||
|
||||
public static String readableTimeDifference(Context context, long time, boolean allowRelative) {
|
||||
|
|
@ -635,6 +637,15 @@ public class UIHelper {
|
|||
ContextCompat.getColor(textView.getContext(), color))));
|
||||
}
|
||||
|
||||
public static String getCallInfo(Context context, Call call) {
|
||||
final boolean received = call.getStatus() == Message.STATUS_RECEIVED;
|
||||
if (!call.isSuccessful() && received) {
|
||||
return context.getString(R.string.missed_call);
|
||||
} else {
|
||||
return context.getString(received ? R.string.incoming_call : R.string.outgoing_call);
|
||||
}
|
||||
}
|
||||
|
||||
public static String filesizeToString(long size) {
|
||||
if (size > (1.5 * 1024 * 1024)) {
|
||||
return Math.round(size * 1f / (1024 * 1024)) + " MiB";
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ public final class Namespace {
|
|||
public static final String HASHES = "urn:xmpp:hashes:2";
|
||||
public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
|
||||
public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
|
||||
public static final String PUBSUB_SOCIAL_FEED = "urn:xmpp:pubsub-social-feed:1";
|
||||
public static final String MICROBLOG = "urn:xmpp:microblog:0";
|
||||
public static final String PUBSUB_STORIES = "urn:xmpp:pubsub-social-feed:stories:0";
|
||||
public static final String ATOM = "http://www.w3.org/2005/Atom";
|
||||
|
||||
public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
|
||||
public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package eu.siacs.conversations.xmpp;
|
|||
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
|
|
@ -15,7 +14,6 @@ import android.util.Pair;
|
|||
import android.util.SparseArray;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Optional;
|
||||
|
|
@ -2115,12 +2113,11 @@ public class XmppConnection implements Runnable {
|
|||
return;
|
||||
}
|
||||
clearIqCallbacks();
|
||||
|
||||
// New resource setting
|
||||
Context context = mXmppConnectionService.getApplicationContext();
|
||||
String clientResource = getEffectiveClientResource(context);
|
||||
account.setResource(clientResource);
|
||||
|
||||
if (account.getJid().isBareJid()) {
|
||||
account.setResource(createNewResource());
|
||||
} else {
|
||||
fixResource(mXmppConnectionService, account);
|
||||
}
|
||||
final Iq iq = new Iq(Iq.Type.SET);
|
||||
final String resource =
|
||||
Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND
|
||||
|
|
@ -2165,7 +2162,7 @@ public class XmppConnection implements Runnable {
|
|||
if (packet.getType() == Iq.Type.ERROR
|
||||
&& error != null
|
||||
&& error.hasChild("conflict")) {
|
||||
account.setResource(clientResource);
|
||||
account.setResource(createNewResource());
|
||||
}
|
||||
throw new StateChangingError(Account.State.BIND_FAILURE);
|
||||
}
|
||||
|
|
@ -2510,9 +2507,7 @@ public class XmppConnection implements Runnable {
|
|||
if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
|
||||
this.appSettings.resetInstallationId();
|
||||
}
|
||||
Context context = mXmppConnectionService.getApplicationContext();
|
||||
String clientResource = getEffectiveClientResource(context);
|
||||
account.setResource(clientResource);
|
||||
account.setResource(createNewResource());
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
account.getJid().asBareJid()
|
||||
|
|
@ -2642,6 +2637,10 @@ public class XmppConnection implements Runnable {
|
|||
tagWriter.writeTag(stream, flush);
|
||||
}
|
||||
|
||||
private static String createNewResource() {
|
||||
return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
|
||||
}
|
||||
|
||||
public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
|
||||
return sendIqPacket(packet, callback, null);
|
||||
}
|
||||
|
|
@ -3351,31 +3350,4 @@ public class XmppConnection implements Runnable {
|
|||
return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the custom resource from SharedPreferences or use a default one
|
||||
*/
|
||||
private String getEffectiveClientResource(Context context) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String customResource = sharedPreferences.getString("custom_resource_name", null);
|
||||
|
||||
if (customResource != null && !customResource.trim().isEmpty()) {
|
||||
Log.d(Config.LOGTAG, "Using custom resource: " + customResource);
|
||||
return String.format("%s.%s", customResource, CryptoHelper.random(3));
|
||||
} else {
|
||||
// Fallback to original default resource generation logic
|
||||
String appName = "monocles chat"; // A sensible default
|
||||
try {
|
||||
// appName = context.getString(R.string.app_name);
|
||||
appName = BuildConfig.APP_NAME + " " + BuildConfig.VERSION_NAME;
|
||||
} catch (Exception e) {
|
||||
Log.w(Config.LOGTAG, "Could not get appName from BuildConfig, using default.",e);
|
||||
}
|
||||
String defaultResource = String.format("%s.%s", appName, CryptoHelper.random(3));
|
||||
// String defaultResource = String.format("%s", appName);
|
||||
Log.d(Config.LOGTAG, "Using default generated resource: " + defaultResource);
|
||||
return defaultResource;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,14 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
connection = new JingleFileTransferConnection(this, id, from);
|
||||
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
|
||||
&& isUsingClearNet(account)) {
|
||||
// START of the fix for session-initiate
|
||||
final Contact contact = account.getRoster().getContact(packet.getFrom());
|
||||
if (contact != null && contact.areCallsDisabled()) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejecting session-initiate with disabled contact " + id.with);
|
||||
sendDecline(account, packet, id);
|
||||
return;
|
||||
}
|
||||
// END of the fix for session-initiate
|
||||
final boolean sessionEnded =
|
||||
this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
|
||||
final boolean stranger =
|
||||
|
|
@ -292,6 +300,21 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("propose".equals(message.getName())) {
|
||||
final Contact contact = account.getRoster().getContact(from);
|
||||
if (contact != null && contact.areCallsDisabled()) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring incoming call (propose) from disabled contact " + from);
|
||||
final var rejection = new im.conversations.android.xmpp.model.stanza.Message();
|
||||
rejection.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
|
||||
rejection.setTo(from);
|
||||
rejection.addChild("reject", Namespace.JINGLE_MESSAGE).setAttribute("id", sessionId);
|
||||
rejection.addChild("store", "urn:xmpp:hints");
|
||||
mXmppConnectionService.sendMessagePacket(account, rejection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ("accept".equals(message.getName()) || "reject".equals(message.getName())) {
|
||||
for (AbstractJingleConnection connection : connections.values()) {
|
||||
if (connection instanceof JingleRtpConnection rtpConnection) {
|
||||
|
|
@ -1290,4 +1313,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDecline(
|
||||
final Account account, final Iq request, final AbstractJingleConnection.Id id) {
|
||||
mXmppConnectionService.sendIqPacket(
|
||||
account, request.generateResponse(Iq.Type.RESULT), null);
|
||||
final var iq = new Iq(Iq.Type.SET);
|
||||
iq.setTo(id.with);
|
||||
final var sessionTermination =
|
||||
iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
|
||||
sessionTermination.setReason(Reason.DECLINE, null);
|
||||
mXmppConnectionService.sendIqPacket(account, iq, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2921,6 +2921,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
((Conversation) conversational).add(this.message);
|
||||
xmppConnectionService.createMessageAsync(message);
|
||||
xmppConnectionService.updateConversationUi();
|
||||
xmppConnectionService.updateCallLogUi();
|
||||
} else {
|
||||
throw new IllegalStateException("Somehow the conversation in a message was a stub");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ package org.minidns;
|
|||
|
||||
import static org.webrtc.ApplicationContextProvider.getApplicationContext;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.minidns.MiniDnsException.ErrorResponseException;
|
||||
import org.minidns.MiniDnsException.NoQueryPossibleException;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
|
|
@ -23,7 +28,6 @@ import org.minidns.dnsserverlookup.AndroidUsingReflection;
|
|||
import org.minidns.dnsserverlookup.DnsServerLookupMechanism;
|
||||
import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
import org.minidns.util.CollectionsUtil;
|
||||
import org.minidns.util.InetAddressUtil;
|
||||
import org.minidns.util.MultipleIoException;
|
||||
|
||||
|
|
@ -39,11 +43,8 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
|
||||
/**
|
||||
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
|
||||
* This circumvents the missing javax.naming package on android.
|
||||
|
|
@ -52,37 +53,20 @@ public class DnsClient extends AbstractDnsClient {
|
|||
|
||||
static final List<DnsServerLookupMechanism> LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>();
|
||||
|
||||
static final Set<Inet4Address> STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>();
|
||||
static final Set<Inet6Address> STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>();
|
||||
|
||||
static {
|
||||
addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE);
|
||||
addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE);
|
||||
addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE);
|
||||
|
||||
try {
|
||||
Inet4Address dnsforgeV4Dns = InetAddressUtil.ipv4From(eu.siacs.conversations.Conversations.getContext().getString(R.string.default_dns_server_ipv4));
|
||||
STATIC_IPV4_DNS_SERVERS.add(dnsforgeV4Dns);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e);
|
||||
}
|
||||
|
||||
try {
|
||||
Inet6Address dnsforgeV6Dns = InetAddressUtil.ipv6From(eu.siacs.conversations.Conversations.getContext().getString(R.string.default_dns_server_ipv6));
|
||||
STATIC_IPV6_DNS_SERVERS.add(dnsforgeV6Dns);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<String> blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(4));
|
||||
|
||||
private final Set<InetAddress> nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>(4));
|
||||
|
||||
private boolean askForDnssec = false;
|
||||
private boolean askForDnssec = true;
|
||||
private boolean disableResultFilter = false;
|
||||
|
||||
private boolean useHardcodedDnsServers = true;
|
||||
private boolean useHardcodedDnsServers = false;
|
||||
|
||||
/**
|
||||
* Create a new DNS client using the global default cache.
|
||||
|
|
@ -103,36 +87,25 @@ public class DnsClient extends AbstractDnsClient {
|
|||
}
|
||||
|
||||
private List<InetAddress> getServerAddresses() {
|
||||
List<InetAddress> dnsServerAddresses = findDnsAddresses();
|
||||
|
||||
List<InetAddress> hardcodedDnsServers = new ArrayList<>();
|
||||
if (useHardcodedDnsServers) {
|
||||
InetAddress primaryHardcodedDnsServer, secondaryHardcodedDnsServer = null;
|
||||
switch (ipVersionSetting) {
|
||||
case v4v6:
|
||||
primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
|
||||
secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
|
||||
break;
|
||||
case v6v4:
|
||||
primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
|
||||
secondaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
|
||||
break;
|
||||
case v4only:
|
||||
primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
|
||||
break;
|
||||
case v6only:
|
||||
primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unknown ipVersionSetting: " + ipVersionSetting);
|
||||
InetAddress primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
|
||||
if (primaryHardcodedDnsServer != null) {
|
||||
hardcodedDnsServers.add(primaryHardcodedDnsServer);
|
||||
}
|
||||
|
||||
dnsServerAddresses.add(primaryHardcodedDnsServer);
|
||||
InetAddress secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
|
||||
if (secondaryHardcodedDnsServer != null) {
|
||||
dnsServerAddresses.add(secondaryHardcodedDnsServer);
|
||||
hardcodedDnsServers.add(secondaryHardcodedDnsServer);
|
||||
}
|
||||
}
|
||||
|
||||
return dnsServerAddresses;
|
||||
if (!hardcodedDnsServers.isEmpty()) {
|
||||
// If custom servers are defined, use them exclusively.
|
||||
return hardcodedDnsServers;
|
||||
} else {
|
||||
// Otherwise, fall back to the system's DNS servers.
|
||||
return findDnsAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -150,6 +123,10 @@ public class DnsClient extends AbstractDnsClient {
|
|||
|
||||
List<InetAddress> dnsServerAddresses = getServerAddresses();
|
||||
|
||||
if (dnsServerAddresses.isEmpty()) {
|
||||
throw new NoQueryPossibleException(q);
|
||||
}
|
||||
|
||||
List<IOException> ioExceptions = new ArrayList<>(dnsServerAddresses.size());
|
||||
for (InetAddress dns : dnsServerAddresses) {
|
||||
if (nonRaServers.contains(dns)) {
|
||||
|
|
@ -179,22 +156,22 @@ public class DnsClient extends AbstractDnsClient {
|
|||
}
|
||||
|
||||
switch (responseMessage.responseCode) {
|
||||
case NO_ERROR:
|
||||
case NX_DOMAIN:
|
||||
break;
|
||||
default:
|
||||
String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: "
|
||||
+ responseMessage.responseCode + '.';
|
||||
if (!LOGGER.isLoggable(Level.FINE)) {
|
||||
// Only append the responseMessage is log level is not fine. If it is fine or higher, the
|
||||
// response has already been logged.
|
||||
warning += "\n" + responseMessage;
|
||||
}
|
||||
LOGGER.warning(warning);
|
||||
case NO_ERROR:
|
||||
case NX_DOMAIN:
|
||||
break;
|
||||
default:
|
||||
String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: "
|
||||
+ responseMessage.responseCode + '.';
|
||||
if (!LOGGER.isLoggable(Level.FINE)) {
|
||||
// Only append the responseMessage is log level is not fine. If it is fine or higher, the
|
||||
// response has already been logged.
|
||||
warning += "\n" + responseMessage;
|
||||
}
|
||||
LOGGER.warning(warning);
|
||||
|
||||
ErrorResponseException exception = new ErrorResponseException(q, dnsQueryResult);
|
||||
ioExceptions.add(exception);
|
||||
continue;
|
||||
ErrorResponseException exception = new ErrorResponseException(q, dnsQueryResult);
|
||||
ioExceptions.add(exception);
|
||||
continue;
|
||||
}
|
||||
|
||||
return dnsQueryResult;
|
||||
|
|
@ -259,7 +236,7 @@ public class DnsClient extends AbstractDnsClient {
|
|||
} catch (SecurityException exception) {
|
||||
LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception);
|
||||
}
|
||||
if (res == null) {
|
||||
if (res == null || res.isEmpty()) {
|
||||
LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server");
|
||||
continue;
|
||||
}
|
||||
|
|
@ -279,15 +256,7 @@ public class DnsClient extends AbstractDnsClient {
|
|||
new Object[] { mechanism.getName(), dnsServers });
|
||||
}
|
||||
|
||||
assert !res.isEmpty();
|
||||
|
||||
// We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial
|
||||
// though, because the list returned by the server mechanism is rather short.
|
||||
|
||||
// Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else,
|
||||
// especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using
|
||||
// getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside
|
||||
// of its DNSSEC guarantees.
|
||||
// Verify the returned DNS servers: Ensure that only valid IP addresses are returned.
|
||||
Iterator<String> it = res.iterator();
|
||||
while (it.hasNext()) {
|
||||
String potentialDnsServer = it.next();
|
||||
|
|
@ -297,7 +266,7 @@ public class DnsClient extends AbstractDnsClient {
|
|||
it.remove();
|
||||
} else if (blacklistedDnsServers.contains(potentialDnsServer)) {
|
||||
LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName()
|
||||
+ "' returned a blacklisted result: '" + potentialDnsServer + "'");
|
||||
+ "' returned a blacklisted result: '" + potentialDnsServer + "'");
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +276,7 @@ public class DnsClient extends AbstractDnsClient {
|
|||
}
|
||||
|
||||
LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
|
||||
+ "' returned not a single valid IP address after sanitazion");
|
||||
+ "' returned not a single valid IP address after sanitazion");
|
||||
res = null;
|
||||
}
|
||||
|
||||
|
|
@ -346,9 +315,7 @@ public class DnsClient extends AbstractDnsClient {
|
|||
|
||||
int validServerAddresses = 0;
|
||||
for (String dnsServerString : res) {
|
||||
// The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings
|
||||
// representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being
|
||||
// involved. Something we want to avoid.
|
||||
// The following invariant must hold: "dnsServerString is a IP address".
|
||||
assert InetAddressUtil.isIpAddress(dnsServerString);
|
||||
|
||||
InetAddress dnsServerAddress;
|
||||
|
|
@ -380,20 +347,20 @@ public class DnsClient extends AbstractDnsClient {
|
|||
List<InetAddress> dnsServers = new ArrayList<>(validServerAddresses);
|
||||
|
||||
switch (setting) {
|
||||
case v4v6:
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
break;
|
||||
case v6v4:
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
break;
|
||||
case v4only:
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
break;
|
||||
case v6only:
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
break;
|
||||
case v4v6:
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
break;
|
||||
case v6v4:
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
break;
|
||||
case v4only:
|
||||
dnsServers.addAll(ipv4DnsServer);
|
||||
break;
|
||||
case v6only:
|
||||
dnsServers.addAll(ipv6DnsServer);
|
||||
break;
|
||||
}
|
||||
return dnsServers;
|
||||
}
|
||||
|
|
@ -404,15 +371,10 @@ public class DnsClient extends AbstractDnsClient {
|
|||
return;
|
||||
}
|
||||
synchronized (LOOKUP_MECHANISMS) {
|
||||
// We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace
|
||||
// LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see
|
||||
// http://stackoverflow.com/a/34827492/194894
|
||||
// TODO: Remove that workaround once MiniDNS is Java 8 only.
|
||||
ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1);
|
||||
tempList.addAll(LOOKUP_MECHANISMS);
|
||||
tempList.add(dnsServerLookup);
|
||||
|
||||
// Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7.
|
||||
Collections.sort(tempList);
|
||||
|
||||
LOOKUP_MECHANISMS.clear();
|
||||
|
|
@ -459,11 +421,29 @@ public class DnsClient extends AbstractDnsClient {
|
|||
}
|
||||
|
||||
public InetAddress getRandomHardcodedIpv4DnsServer() {
|
||||
return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom);
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
String dnsServer = preferences.getString("dns_server_ipv4", null);
|
||||
if (TextUtils.isEmpty(dnsServer)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return InetAddressUtil.ipv4From(dnsServer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public InetAddress getRandomHarcodedIpv6DnsServer() {
|
||||
return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom);
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
String dnsServer = preferences.getString("dns_server_ipv6", null);
|
||||
if (TextUtils.isEmpty(dnsServer)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return InetAddressUtil.ipv6From(dnsServer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Question getReverseIpLookupQuestionFor(DnsName dnsName) {
|
||||
|
|
@ -491,6 +471,6 @@ public class DnsClient extends AbstractDnsClient {
|
|||
throw new IllegalArgumentException("The provided inetAddress '" + inetAddress
|
||||
+ "' is neither of type Inet4Address nor Inet6Address");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,10 @@ package org.minidns.constants;
|
|||
|
||||
import static org.webrtc.ApplicationContextProvider.getApplicationContext;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.minidns.util.InetAddressUtil;
|
||||
|
||||
|
|
@ -46,9 +49,9 @@ public class DnsRootServer {
|
|||
rootServerInet4Address('k', 193, 0, 14, 129),
|
||||
rootServerInet4Address('l', 199, 7, 83, 42),
|
||||
rootServerInet4Address('m', 202, 12, 27, 33),
|
||||
};
|
||||
};
|
||||
|
||||
protected static final Inet6Address[] IPV6_ROOT_SERVERS = new Inet6Address[] {
|
||||
protected static final Inet6Address[] IPV6_ROOT_SERVERS = new Inet6Address[] {
|
||||
rootServerInet6Address('a', 0x2001, 0x0503, 0xba3e, 0x0000, 0x0000, 0x000, 0x0002, 0x0030),
|
||||
rootServerInet6Address('b', 0x2001, 0x0500, 0x0084, 0x0000, 0x0000, 0x000, 0x0000, 0x000b),
|
||||
rootServerInet6Address('c', 0x2001, 0x0500, 0x0002, 0x0000, 0x0000, 0x000, 0x0000, 0x000c),
|
||||
|
|
@ -59,65 +62,75 @@ public class DnsRootServer {
|
|||
rootServerInet6Address('j', 0x2001, 0x0503, 0x0c27, 0x0000, 0x0000, 0x000, 0x0002, 0x0030),
|
||||
rootServerInet6Address('l', 0x2001, 0x0500, 0x0003, 0x0000, 0x0000, 0x000, 0x0000, 0x0042),
|
||||
rootServerInet6Address('m', 0x2001, 0x0dc3, 0x0000, 0x0000, 0x0000, 0x000, 0x0000, 0x0035),
|
||||
};
|
||||
};
|
||||
|
||||
private static Inet4Address rootServerInet4Address(char rootServerId, int addr0, int addr1, int addr2, int addr3) {
|
||||
Inet4Address inetAddress;
|
||||
String name = rootServerId + ".root-servers.net";
|
||||
try {
|
||||
inetAddress = (Inet4Address) InetAddress.getByAddress(name, new byte[] { (byte) addr0, (byte) addr1, (byte) addr2,
|
||||
(byte) addr3 });
|
||||
IPV4_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
|
||||
} catch (UnknownHostException e) {
|
||||
// This should never happen, if it does it's our fault!
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return inetAddress;
|
||||
private static Inet4Address rootServerInet4Address(char rootServerId, int addr0, int addr1, int addr2, int addr3) {
|
||||
Inet4Address inetAddress;
|
||||
String name = rootServerId + ".root-servers.net";
|
||||
try {
|
||||
inetAddress = (Inet4Address) InetAddress.getByAddress(name, new byte[] { (byte) addr0, (byte) addr1, (byte) addr2,
|
||||
(byte) addr3 });
|
||||
IPV4_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
|
||||
} catch (UnknownHostException e) {
|
||||
// This should never happen, if it does it's our fault!
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
private static Inet6Address rootServerInet6Address(char rootServerId, int addr0, int addr1, int addr2, int addr3, int addr4, int addr5, int addr6, int addr7) {
|
||||
Inet6Address inetAddress;
|
||||
String name = rootServerId + ".root-servers.net";
|
||||
try {
|
||||
inetAddress = (Inet6Address) InetAddress.getByAddress(name, new byte[] {
|
||||
// @formatter:off
|
||||
(byte) (addr0 >> 8), (byte) addr0, (byte) (addr1 >> 8), (byte) addr1,
|
||||
(byte) (addr2 >> 8), (byte) addr2, (byte) (addr3 >> 8), (byte) addr3,
|
||||
(byte) (addr4 >> 8), (byte) addr4, (byte) (addr5 >> 8), (byte) addr5,
|
||||
(byte) (addr6 >> 8), (byte) addr6, (byte) (addr7 >> 8), (byte) addr7
|
||||
// @formatter:on
|
||||
});
|
||||
IPV6_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
|
||||
} catch (UnknownHostException e) {
|
||||
// This should never happen, if it does it's our fault!
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return inetAddress;
|
||||
}
|
||||
return inetAddress;
|
||||
}
|
||||
|
||||
public static Inet4Address getRandomIpv4RootServer(Random random) {
|
||||
if (getApplicationContext().getString(R.string.default_dns_server_ipv4).equals("194.242.2.2")) {
|
||||
return IPV4_ROOT_SERVERS[random.nextInt(IPV4_ROOT_SERVERS.length)];
|
||||
} else {
|
||||
return InetAddressUtil.ipv4From(eu.siacs.conversations.Conversations.getContext().getString(R.string.default_dns_server_ipv4));
|
||||
private static Inet6Address rootServerInet6Address(char rootServerId, int addr0, int addr1, int addr2, int addr3, int addr4, int addr5, int addr6, int addr7) {
|
||||
Inet6Address inetAddress;
|
||||
String name = rootServerId + ".root-servers.net";
|
||||
try {
|
||||
inetAddress = (Inet6Address) InetAddress.getByAddress(name, new byte[] {
|
||||
// @formatter:off
|
||||
(byte) (addr0 >> 8), (byte) addr0, (byte) (addr1 >> 8), (byte) addr1,
|
||||
(byte) (addr2 >> 8), (byte) addr2, (byte) (addr3 >> 8), (byte) addr3,
|
||||
(byte) (addr4 >> 8), (byte) addr4, (byte) (addr5 >> 8), (byte) addr5,
|
||||
(byte) (addr6 >> 8), (byte) addr6, (byte) (addr7 >> 8), (byte) addr7
|
||||
// @formatter:on
|
||||
});
|
||||
IPV6_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
|
||||
} catch (UnknownHostException e) {
|
||||
// This should never happen, if it does it's our fault!
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return inetAddress;
|
||||
}
|
||||
|
||||
public static Inet4Address getRandomIpv4RootServer(Random random) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
String dnsServer = preferences.getString("dns_server_ipv4", null);
|
||||
if (!TextUtils.isEmpty(dnsServer)) {
|
||||
try {
|
||||
return InetAddressUtil.ipv4From(dnsServer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid format, do not fall back to root servers
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Inet6Address getRandomIpv6RootServer(Random random) {
|
||||
if (getApplicationContext().getString(R.string.default_dns_server_ipv6).equals("[2a07:e340::2]")) {
|
||||
return IPV6_ROOT_SERVERS[random.nextInt(IPV6_ROOT_SERVERS.length)];
|
||||
} else {
|
||||
return InetAddressUtil.ipv6From(eu.siacs.conversations.Conversations.getContext().getString(R.string.default_dns_server_ipv6));
|
||||
public static Inet6Address getRandomIpv6RootServer(Random random) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
String dnsServer = preferences.getString("dns_server_ipv6", null);
|
||||
if (!TextUtils.isEmpty(dnsServer)) {
|
||||
try {
|
||||
return InetAddressUtil.ipv6From(dnsServer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid format, do not fall back to root servers
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Inet4Address getIpv4RootServerById(char id) {
|
||||
return IPV4_ROOT_SERVER_MAP.get(id);
|
||||
}
|
||||
public static Inet4Address getIpv4RootServerById(char id) {
|
||||
return IPV4_ROOT_SERVER_MAP.get(id);
|
||||
}
|
||||
|
||||
public static Inet6Address getIpv6RootServerById(char id) {
|
||||
return IPV6_ROOT_SERVER_MAP.get(id);
|
||||
}
|
||||
public static Inet6Address getIpv6RootServerById(char id) {
|
||||
return IPV6_ROOT_SERVER_MAP.get(id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
5
src/main/res/drawable/calls_selected_black_24dp.xml
Normal file
5
src/main/res/drawable/calls_selected_black_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/calls_selected_white_24dp.xml
Normal file
5
src/main/res/drawable/calls_selected_white_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/calls_unselected_black_24dp.xml
Normal file
5
src/main/res/drawable/calls_unselected_black_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6.54,5c0.06,0.89 0.21,1.76 0.45,2.59l-1.2,1.2c-0.41,-1.2 -0.67,-2.47 -0.76,-3.79h1.51m9.86,12.02c0.85,0.24 1.72,0.39 2.6,0.45v1.49c-1.32,-0.09 -2.59,-0.35 -3.8,-0.75l1.2,-1.19M7.5,3H4c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.49c0,-0.55 -0.45,-1 -1,-1 -1.24,0 -2.45,-0.2 -3.57,-0.57 -0.1,-0.04 -0.21,-0.05 -0.31,-0.05 -0.26,0 -0.51,0.1 -0.71,0.29l-2.2,2.2c-2.83,-1.45 -5.15,-3.76 -6.59,-6.59l2.2,-2.2c0.28,-0.28 0.36,-0.67 0.25,-1.02C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/calls_unselected_white_24dp.xml
Normal file
5
src/main/res/drawable/calls_unselected_white_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6.54,5c0.06,0.89 0.21,1.76 0.45,2.59l-1.2,1.2c-0.41,-1.2 -0.67,-2.47 -0.76,-3.79h1.51m9.86,12.02c0.85,0.24 1.72,0.39 2.6,0.45v1.49c-1.32,-0.09 -2.59,-0.35 -3.8,-0.75l1.2,-1.19M7.5,3H4c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.49c0,-0.55 -0.45,-1 -1,-1 -1.24,0 -2.45,-0.2 -3.57,-0.57 -0.1,-0.04 -0.21,-0.05 -0.31,-0.05 -0.26,0 -0.51,0.1 -0.71,0.29l-2.2,2.2c-2.83,-1.45 -5.15,-3.76 -6.59,-6.59l2.2,-2.2c0.28,-0.28 0.36,-0.67 0.25,-1.02C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/delete_18dp.xml
Normal file
5
src/main/res/drawable/delete_18dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="18dp" android:tint="?colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="18dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/favorite_border_24.xml
Normal file
5
src/main/res/drawable/favorite_border_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/favorite_filled_24.xml
Normal file
5
src/main/res/drawable/favorite_filled_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/feed_selected_black_24dp.xml
Normal file
5
src/main/res/drawable/feed_selected_black_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8L16,3zM7,7h5v2H7V7zM17,17H7v-2h10V17zM17,13H7v-2h10V13zM15,9V5l4,4H15z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/feed_selected_white_24dp.xml
Normal file
5
src/main/res/drawable/feed_selected_white_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8L16,3zM7,7h5v2H7V7zM17,17H7v-2h10V17zM17,13H7v-2h10V13zM15,9V5l4,4H15z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/feed_unselected_black_24dp.xml
Normal file
5
src/main/res/drawable/feed_unselected_black_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8L16,3zM19,19H5V5h10v4h4V19zM7,17h10v-2H7V17zM12,7H7v2h5V7zM7,13h10v-2H7V13z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/feed_unselected_white_24dp.xml
Normal file
5
src/main/res/drawable/feed_unselected_white_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8L16,3zM19,19H5V5h10v4h4V19zM7,17h10v-2H7V17zM12,7H7v2h5V7zM7,13h10v-2H7V13z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<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"/>
|
||||
|
||||
|
|
|
|||
5
src/main/res/drawable/outline_add_a_photo_24.xml
Normal file
5
src/main/res/drawable/outline_add_a_photo_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440,520L440,520L440,520L440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,280Q40,247 63.5,223.5Q87,200 120,200L246,200L320,120L560,120L560,200L355,200L282,280L120,280Q120,280 120,280Q120,280 120,280L120,760Q120,760 120,760Q120,760 120,760L760,760Q760,760 760,760Q760,760 760,760L760,400L840,400L840,760Q840,793 816.5,816.5Q793,840 760,840L120,840ZM760,280L760,200L680,200L680,120L760,120L760,40L840,40L840,120L920,120L920,200L840,200L840,280L760,280ZM440,700Q515,700 567.5,647.5Q620,595 620,520Q620,445 567.5,392.5Q515,340 440,340Q365,340 312.5,392.5Q260,445 260,520Q260,595 312.5,647.5Q365,700 440,700ZM440,620Q398,620 369,591Q340,562 340,520Q340,478 369,449Q398,420 440,420Q482,420 511,449Q540,478 540,520Q540,562 511,591Q482,620 440,620Z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/outline_amp_stories_24.xml
Normal file
5
src/main/res/drawable/outline_amp_stories_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M280,800L280,160L680,160L680,800L280,800ZM120,720L120,240L200,240L200,720L120,720ZM760,720L760,240L840,240L840,720L760,720ZM360,720L600,720L600,240L360,240L360,720ZM360,720L360,240L360,240L360,720Z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/outline_heart_plus_24.xml
Normal file
5
src/main/res/drawable/outline_heart_plus_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459L440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459ZM440,840L313,726Q241,661 189.5,610Q138,559 104.5,514Q71,469 55.5,427Q40,385 40,339Q40,245 103,182.5Q166,120 260,120Q312,120 359,142Q406,164 440,204Q474,164 521,142Q568,120 620,120Q701,120 756,165.5Q811,211 831,280Q831,280 817.5,280Q804,280 788.5,280Q773,280 759.5,280Q746,280 746,280Q728,240 693,220Q658,200 620,200Q569,200 532,227.5Q495,255 463,300L417,300Q386,255 346.5,227.5Q307,200 260,200Q203,200 161.5,239.5Q120,279 120,339Q120,372 134,406Q148,440 184,484.5Q220,529 282,588.5Q344,648 440,732Q466,709 501,679Q536,649 557,629Q557,629 566,638Q575,647 585.5,657.5Q596,668 605,677Q614,686 614,686Q592,706 558,735.5Q524,765 498,788L440,840ZM720,680L720,560L600,560L600,480L720,480L720,360L800,360L800,480L920,480L920,560L800,560L800,680L720,680Z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/outline_keyboard_arrow_down_24.xml
Normal file
5
src/main/res/drawable/outline_keyboard_arrow_down_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/outline_newspaper_24.xml
Normal file
5
src/main/res/drawable/outline_newspaper_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8L16,3zM19,19H5V5h10v4h4V19zM7,17h10v-2H7V17zM12,7H7v2h5V7zM7,13h10v-2H7V13z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/outline_stories_24.xml
Normal file
5
src/main/res/drawable/outline_stories_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2H4C2.9,2 2,2.9 2,4v10h2V4h10V2zM18,6H8C6.9,6 6,6.9 6,8v10h2V8h10V6zM20,10h-8c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2v-8C22,10.9 21.1,10 20,10zM20,20h-8v-8h8V20z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/stories_selected_black_24.xml
Normal file
5
src/main/res/drawable/stories_selected_black_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2L4,2c-1.11,0 -2,0.9 -2,2v10h2L4,4h10L14,2zM18,6L8,6c-1.11,0 -2,0.9 -2,2v10h2L8,8h10L18,6zM20,10h-8c-1.11,0 -2,0.9 -2,2v8c0,1.1 0.89,2 2,2h8c1.1,0 2,-0.9 2,-2v-8c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/stories_selected_white_24.xml
Normal file
5
src/main/res/drawable/stories_selected_white_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2L4,2c-1.11,0 -2,0.9 -2,2v10h2L4,4h10L14,2zM18,6L8,6c-1.11,0 -2,0.9 -2,2v10h2L8,8h10L18,6zM20,10h-8c-1.11,0 -2,0.9 -2,2v8c0,1.1 0.89,2 2,2h8c1.1,0 2,-0.9 2,-2v-8c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/stories_unselected_black_24.xml
Normal file
5
src/main/res/drawable/stories_unselected_black_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2H4C2.9,2 2,2.9 2,4v10h2V4h10V2zM18,6H8C6.9,6 6,6.9 6,8v10h2V8h10V6zM20,10h-8c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2v-8C22,10.9 21.1,10 20,10zM20,20h-8v-8h8V20z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/stories_unselected_white_24.xml
Normal file
5
src/main/res/drawable/stories_unselected_white_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2H4C2.9,2 2,2.9 2,4v10h2V4h10V2zM18,6H8C6.9,6 6,6.9 6,8v10h2V8h10V6zM20,10h-8c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2v-8C22,10.9 21.1,10 20,10zM20,20h-8v-8h8V20z"/>
|
||||
|
||||
</vector>
|
||||
5
src/main/res/drawable/story_reply_background.xml
Normal file
5
src/main/res/drawable/story_reply_background.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/colorSurfaceVariant" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
50
src/main/res/layout/activity_calls.xml
Normal file
50
src/main/res/layout/activity_calls.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/bottom_navigation">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/calls" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="?colorSurfaceContainerLowest"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:menu="@menu/bottom_navigation_menu_calls" />
|
||||
</RelativeLayout>
|
||||
</layout>
|
||||
|
|
@ -163,6 +163,12 @@
|
|||
android:textColor="@color/black87"
|
||||
android:text="@string/add_contact" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/follow_feed_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/follow_feed" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/details_send_presence"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -175,6 +181,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/receive_presence_updates" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/disable_calls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/disable_voice_and_video_calls" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
|
|
|
|||
209
src/main/res/layout/activity_create_post.xml
Normal file
209
src/main/res/layout/activity_create_post.xml
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.CreatePostActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/app_bar"
|
||||
app:layout_constraintBottom_toTopOf="@+id/attachment_options">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/post_preview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/post_preview_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="This is the original post title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/post_preview_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/post_preview_title"
|
||||
tools:text="This is a preview of the original post content that you are replying to." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/posts_attachment_preview_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/post_preview">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/attachment_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="150dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/avatars"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/attachment_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="150dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
tools:src="@tools:sample/avatars"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/account_spinner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/posts_attachment_preview_layout" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/post_title_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/account_spinner">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/post_title_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/post_title"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textColorHint="?android:attr/textColorHint"
|
||||
android:textColorHighlight="?attr/colorControlHighlight"
|
||||
android:textColorLink="?attr/colorAccent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/post_content_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/post_title_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/post_content_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:minLines="5"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textColorHint="?android:attr/textColorHint"
|
||||
android:textColorHighlight="?attr/colorControlHighlight"
|
||||
android:textColorLink="?attr/colorAccent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachment_options"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/publish_button">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attach_file_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_attach_file_24dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attach_image_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_camera_alt_24dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attach_video_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_videocam_24dp"
|
||||
android:contentDescription="@string/attach_record_video" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/publish_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/publish"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_above="@id/bottom_navigation"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
|
|
@ -85,16 +84,6 @@
|
|||
android:src="@drawable/ic_settings_24dp"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Photo" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="?colorSurfaceContainerLowest"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:menu="@menu/bottom_navigation_menu_accounts" />
|
||||
</RelativeLayout>
|
||||
</layout>
|
||||
|
|
|
|||
112
src/main/res/layout/activity_posts.xml
Normal file
112
src/main/res/layout/activity_posts.xml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true" >
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/bottom_navigation">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.PostsActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/action_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/follow_suggestions_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:visibility="visible"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/follow_suggestions_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/follow_suggestions" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/toggle_suggestions_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/toggle_suggestions"
|
||||
android:src="@drawable/outline_keyboard_arrow_down_24" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/follow_suggestions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/posts_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/item_post" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_create_post"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_edit_24dp"
|
||||
app:layout_anchor="@id/posts_list"
|
||||
app:layout_anchorGravity="bottom|end" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</RelativeLayout>
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="?colorSurfaceContainerLowest"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:menu="@menu/bottom_navigation_menu_feeds" />
|
||||
</RelativeLayout>
|
||||
|
||||
</layout>
|
||||
|
|
@ -7,11 +7,6 @@
|
|||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/bottom_navigation">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -65,16 +60,6 @@
|
|||
app:sdMainFabOpenedIconColor="?colorOnPrimaryContainer"
|
||||
app:sdOverlayLayout="@id/overlay"
|
||||
app:sdUseReverseAnimationOnClose="true" />
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="?colorSurfaceContainerLowest"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:menu="@menu/bottom_navigation_menu_contacts" />
|
||||
|
||||
</RelativeLayout>
|
||||
</layout>
|
||||
|
|
|
|||
69
src/main/res/layout/activity_stories.xml
Normal file
69
src/main/res/layout/activity_stories.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/bottom_navigation">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/stories_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/placeholder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_add_story"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/outline_add_a_photo_24" />
|
||||
</RelativeLayout>
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="?colorSurfaceContainerLowest"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:menu="@menu/bottom_navigation_menu_stories" />
|
||||
</RelativeLayout>
|
||||
</layout>
|
||||
92
src/main/res/layout/activity_story_view.xml
Normal file
92
src/main/res/layout/activity_story_view.xml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#80000000"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progress_bar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:indeterminate="true"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" >
|
||||
|
||||
<eu.siacs.conversations.ui.widget.AvatarView
|
||||
android:id="@+id/toolbar_avatar"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/toolbar_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/actionbar_text_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/toolbar_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#80000000"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_title_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
android:maxLines="8"
|
||||
android:scrollbars="vertical"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
54
src/main/res/layout/dialog_create_story.xml
Normal file
54
src/main/res/layout/dialog_create_story.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_preview_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/story_preview_video"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/publish_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/title_text_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="500">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/title_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:hint="@string/title_optional"
|
||||
android:inputType="textMultiLine"
|
||||
android:lines="3"
|
||||
android:maxLength="500" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
13
src/main/res/layout/dialog_image_preview.xml
Normal file
13
src/main/res/layout/dialog_image_preview.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
</RelativeLayout>
|
||||
13
src/main/res/layout/dialog_video_preview.xml
Normal file
13
src/main/res/layout/dialog_video_preview.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/video_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</FrameLayout>
|
||||
19
src/main/res/layout/fragment_calls.xml
Normal file
19
src/main/res/layout/fragment_calls.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/no_calls_yet"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -6,57 +6,65 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/overview_snackbar"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/context_preview"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@drawable/snackbar"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overview_snackbar_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_toStartOf="@+id/overview_snackbar_action"
|
||||
android:textColor="?colorOnSurfaceInverse"
|
||||
android:text="Warning" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overview_snackbar_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?colorOnSurfaceInverse"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/action_fix" />
|
||||
</RelativeLayout>
|
||||
|
||||
<de.monocles.chat.ContextMenuRecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/overview_snackbar"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/context_preview"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@drawable/snackbar"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overview_snackbar_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_toStartOf="@+id/overview_snackbar_action"
|
||||
android:textColor="?colorOnSurfaceInverse"
|
||||
android:text="Warning" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overview_snackbar_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?colorOnSurfaceInverse"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/action_fix" />
|
||||
</RelativeLayout>
|
||||
|
||||
<de.monocles.chat.ContextMenuRecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_start_conversation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_edit_24dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -65,6 +73,7 @@
|
|||
android:layout_margin="16dp"
|
||||
android:text="@string/start_chat"
|
||||
app:icon="@drawable/ic_chat_24dp" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
||||
|
|
|
|||
37
src/main/res/layout/fragment_story.xml
Normal file
37
src/main/res/layout/fragment_story.xml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/story_progress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/story_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_image_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progress_bar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="4dp"/>
|
||||
|
||||
</FrameLayout>
|
||||
78
src/main/res/layout/item_call.xml
Normal file
78
src/main/res/layout/item_call.xml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<eu.siacs.conversations.ui.widget.AvatarView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/center_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:layout_toStartOf="@+id/call_again"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginEnd="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="2dp"
|
||||
android:layout_gravity="center"
|
||||
android:padding="2dp"
|
||||
android:background="@color/sd_label_text_color" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/call_again"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_call_24dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
76
src/main/res/layout/item_comment.xml
Normal file
76
src/main/res/layout/item_comment.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<eu.siacs.conversations.ui.widget.AvatarView
|
||||
android:id="@+id/comment_author_avatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/retract_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/retract_comment"
|
||||
android:src="@drawable/delete_18dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/comment_author_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/comment_author_name"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comment_author_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/retract_button"
|
||||
app:layout_constraintStart_toEndOf="@+id/comment_author_avatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/comment_author_avatar"
|
||||
tools:text="Jane Doe" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comment_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="10sp"
|
||||
app:layout_constraintStart_toEndOf="@+id/comment_author_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/comment_author_name"
|
||||
tools:text="1 hour ago" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comment_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:autoLink="web"
|
||||
android:textIsSelectable="true"
|
||||
android:textColorHighlight="?attr/colorControlHighlight"
|
||||
android:textColorLink="?attr/colorAccent"
|
||||
android:textColorHint="?android:attr/textColorHint"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/comment_author_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/comment_timestamp"
|
||||
tools:text="This is a comment." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
33
src/main/res/layout/item_follow_suggestion.xml
Normal file
33
src/main/res/layout/item_follow_suggestion.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="2dp">
|
||||
|
||||
<eu.siacs.conversations.ui.widget.AvatarView
|
||||
android:id="@+id/contact_avatar"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="70dp"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?textAppearanceCaption"
|
||||
tools:text="Contact Name" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/follow_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/follow" />
|
||||
|
||||
</LinearLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue