update fork #128

Manually merged
tristan merged 181 commits from mirror/monocles_chat_clean:master into master 2026-01-23 14:02:38 +01:00
24 changed files with 531 additions and 281 deletions
Showing only changes of commit e3ef4809b3 - Show all commits

Refactor stories into its own activity

Arne 2025-12-31 10:21:40 +01:00

View file

@ -396,6 +396,9 @@
<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" />

View file

@ -1136,6 +1136,13 @@ public class ConversationsActivity extends XmppActivity
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.manageaccounts -> {
Intent i = new Intent(getApplicationContext(), MANAGE_ACCOUNT_ACTIVITY);
i.putExtra("show_nav_bar", true);

View file

@ -32,17 +32,11 @@ package eu.siacs.conversations.ui;
import static androidx.recyclerview.widget.ItemTouchHelper.LEFT;
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
import android.Manifest;
import android.app.Activity;
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@ -52,12 +46,10 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.EditText;
import android.widget.PopupMenu;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -65,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;
@ -73,12 +65,8 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Story;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.ui.adapter.StoryAdapter;
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
@ -88,36 +76,21 @@ 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.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public class ConversationsOverviewFragment extends XmppFragment implements XmppConnectionService.OnStoriesUpdate {
public class ConversationsOverviewFragment extends XmppFragment {
private static final String STATE_SCROLL_POSITION =
ConversationsOverviewFragment.class.getName() + ".scroll_state";
private static final int REQUEST_CHOOSE_STORY_IMAGE = 0x2b01;
private static final int REQUEST_CAMERA_PERMISSION = 0x2b03;
private Account mSelectedAccount;
private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
private final List<Conversation> conversations = new ArrayList<>();
private final List<Story> stories = new ArrayList<>();
private final PendingItem<Conversation> swipedConversation = new PendingItem<>();
private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
private FragmentConversationsOverviewBinding binding;
private ConversationAdapter conversationsAdapter;
private StoryAdapter storyAdapter;
private XmppActivity activity;
private final PendingActionHelper pendingActionHelper = new PendingActionHelper();
@ -323,7 +296,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
super.onDestroyView();
this.binding = null;
this.conversationsAdapter = null;
this.storyAdapter = null;
this.touchHelper = null;
}
@ -337,7 +309,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
public void onPause() {
Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onPause()");
pendingActionHelper.execute();
activity.xmppConnectionService.removeOnStoriesUpdateListener(this);
super.onPause();
}
@ -362,7 +333,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
this.binding.fab.setOnClickListener(
(view) -> StartConversationActivity.launch(getActivity()));
this.binding.fabStory.setOnClickListener(v -> selectAccountToPublishStory());
this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations);
this.conversationsAdapter.setConversationClickListener(
(view, conversation) -> {
@ -377,10 +347,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
this.binding.list.setAdapter(this.conversationsAdapter);
this.binding.list.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
this.storyAdapter = new StoryAdapter(this.activity, this.stories);
this.binding.storiesList.setAdapter(this.storyAdapter);
this.binding.storiesList.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
registerForContextMenu(this.binding.list);
this.binding.list.addOnScrollListener(ExtendedFabSizeChanger.of(binding.fab));
if (activity.getPreferences().getBoolean("swipe_to_archive", true)) this.touchHelper = new ItemTouchHelper(this.callback);
@ -397,24 +363,15 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
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);
final MenuItem stories = menu.findItem(R.id.action_stories);
if (manageAccounts != null) manageAccounts.setVisible(false);
if (settings != null) settings.setVisible(false);
if (stories != null) stories.setVisible(false);
}
if (activity == null || activity.xmppConnectionService == null || activity.xmppConnectionService.getAccounts().size() != 1) {
noteToSelf.setVisible(false);
}
final MenuItem stories = menu.findItem(R.id.action_toggle_stories);
if (stories != null) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean show = preferences.getBoolean("show_stories", true);
if (show) {
stories.setTitle(R.string.hide_stories);
} else {
stories.setTitle(R.string.show_stories);
}
}
}
@Override
@ -486,7 +443,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
@Override
public void onBackendConnected() {
refresh();
activity.xmppConnectionService.setOnStoriesUpdateListener(this);
}
private void setupSwipe() {
@ -527,22 +483,13 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
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 addStory = menu.findItem(R.id.action_add_story);
MenuItem stories = menu.findItem(R.id.action_stories);
if (navBarVisible) {
manageAccount.setVisible(false);
manageAccounts.setVisible(false);
if (stories != null) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean show = preferences.getBoolean("show_stories", true);
if (show) {
addStory.setVisible(true);
} else {
addStory.setVisible(false);
}
}
stories.setVisible(false);
} else {
AccountUtils.showHideMenuItems(menu);
addStory.setVisible(false);
}
}
@ -558,18 +505,8 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
if (showed) {
this.binding.fab.setVisibility(View.GONE);
binding.fabStory.setVisibility(View.GONE);
} else {
this.binding.fab.setVisibility(View.VISIBLE);
if (stories != null) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean show = preferences.getBoolean("show_stories", true);
if (show) {
this.binding.fabStory.setVisibility(View.VISIBLE);
} else {
this.binding.fabStory.setVisibility(View.GONE);
}
}
}
}
}
@ -586,7 +523,8 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
return false;
}
switch (item.getItemId()) {
case R.id.action_search:startActivity(new Intent(getActivity(), SearchActivity.class));
case R.id.action_search:
startActivity(new Intent(getActivity(), SearchActivity.class));
return true;
case R.id.action_easy_invite:
selectAccountToStartEasyInvite();
@ -600,26 +538,12 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
activity.switchToConversation(conversation);
}
return true;
case R.id.action_toggle_stories:
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean show = preferences.getBoolean("show_stories", true);
preferences.edit().putBoolean("show_stories", !show).apply();
refresh();
activity.invalidateOptionsMenu();
return true;
case R.id.action_add_story:
selectAccountToPublishStory();
case R.id.action_stories:
startActivity(new Intent(getActivity(), StoriesActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
private void setShowStories(boolean show) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
preferences.edit().putBoolean("show_stories", show).apply();
refresh();
activity.invalidateOptionsMenu();
}
private void selectAccountToStartEasyInvite() {
final List<Account> accounts =
EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
@ -659,29 +583,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
return;
}
this.stories.clear();
this.stories.addAll(
this.activity.xmppConnectionService.getStories().stream()
.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()));
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean show = preferences.getBoolean("show_stories", true);
if (this.stories.isEmpty() || !show) {
binding.storiesList.setVisibility(View.GONE);
} else {
binding.storiesList.setVisibility(View.VISIBLE);
}
this.storyAdapter.notifyDataSetChanged();
this.activity.populateWithOrderedConversations(this.conversations);
Conversation removed = this.swipedConversation.peek();
if (removed != null) {
@ -698,7 +599,6 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
}
if (activity.xmppConnectionService != null && activity.xmppConnectionService.isOnboarding()) {
binding.fab.setVisibility(View.GONE);
binding.fabStory.setVisibility(View.GONE);
if (this.conversations.size() == 1) {
if (activity instanceof OnConversationSelected) {
@ -713,16 +613,8 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
if (showed) {
this.binding.fab.setVisibility(View.GONE);
this.binding.fabStory.setVisibility(View.GONE);
} else {
this.binding.fab.setVisibility(View.VISIBLE);
if (stories != null) {
if (show) {
this.binding.fabStory.setVisibility(View.VISIBLE);
} else {
this.binding.fabStory.setVisibility(View.GONE);
}
}
}
}
}
@ -773,127 +665,4 @@ public class ConversationsOverviewFragment extends XmppFragment implements XmppC
}
}
@Override
public void onStoriesUpdate() {
activity.runOnUiThread(this::refresh);
}
@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 {
uri = null;
}
if (uri != null && mSelectedAccount != null) {
final String mimeType = activity.getContentResolver().getType(uri);
final EditText input = new EditText(getActivity());
input.setHint(R.string.title_optional);
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.add_story_title)
.setView(input)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.publish, (dialog, which) -> {
final String title = input.getText().toString();
Toast.makeText(activity, R.string.uploading_story, Toast.LENGTH_SHORT).show();
activity.xmppConnectionService.uploadFileForUrl(mSelectedAccount, uri, mimeType, new UiCallback<String>() {
@Override
public void success(String url) {
activity.xmppConnectionService.publishStory(mSelectedAccount, url, mimeType, title, new UiCallback<Void>() {
@Override
public void success(Void aVoid) {
activity.runOnUiThread(() -> Toast.makeText(activity, R.string.story_published, Toast.LENGTH_SHORT).show());
}
@Override
public void error(int errorCode, Void object) {
activity.runOnUiThread(() -> Toast.makeText(activity, errorCode, Toast.LENGTH_SHORT).show());
}
@Override
public void userInputRequired(android.app.PendingIntent pi, Void object) {
// not used
}
});
}
@Override
public void error(int errorCode, String object) {
activity.runOnUiThread(() -> Toast.makeText(activity, errorCode, Toast.LENGTH_SHORT).show());
}
@Override
public void userInputRequired(android.app.PendingIntent pi, String object) {
// not used
}
});
})
.create()
.show();
}
}
} else {
pendingTakePhotoUri.pop();
}
}
private void selectAccountToPublishStory() {
final List<Account> accounts = activity.xmppConnectionService.getAccounts().stream().filter(Account::isEnabled).collect(Collectors.toList());
if (accounts.isEmpty()) {
Toast.makeText(getActivity(), 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(activity);
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 (activity.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/*");
final Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
final Uri takePhotoUri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri();
pendingTakePhotoUri.push(takePhotoUri);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, takePhotoUri);
final Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.perform_action_with));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{cameraIntent});
try {
startActivityForResult(chooserIntent, REQUEST_CHOOSE_STORY_IMAGE);
} catch (final ActivityNotFoundException e) {
Toast.makeText(activity, 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);
}
}
}
}

View file

@ -452,6 +452,13 @@ public class StartConversationActivity extends XmppActivity
case R.id.contactslist -> {
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.manageaccounts -> {
Intent i = new Intent(getApplicationContext(), MANAGE_ACCOUNT_ACTIVITY);
i.putExtra("show_nav_bar", true);

View file

@ -0,0 +1,331 @@
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.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.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.databinding.DataBindingUtil;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
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.Story;
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 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);
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.contactslist -> {
Intent i = new Intent(getApplicationContext(), StartConversationActivity.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.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());
}
});
}
@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);
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);
}
@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 {
uri = null;
}
if (uri != null && mSelectedAccount != null) {
final String mimeType = getContentResolver().getType(uri);
final EditText input = new EditText(this);
input.setHint(R.string.title_optional);
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.add_story_title)
.setView(input)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.publish, (dialog, which) -> {
final String title = input.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(xmppConnectionService, R.string.story_published, Toast.LENGTH_SHORT).show());
}
@Override
public void error(int errorCode, Void object) {
runOnUiThread(() -> Toast.makeText(xmppConnectionService, 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(xmppConnectionService, errorCode, Toast.LENGTH_SHORT).show());
}
@Override
public void userInputRequired(PendingIntent pi, String object) {
// not used
}
});
})
.create()
.show();
}
}
} else {
pendingTakePhotoUri.pop();
}
}
private void selectAccountToPublishStory() {
final List<Account> accounts = xmppConnectionService.getAccounts().stream().filter(Account::isEnabled).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/*");
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 chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.perform_action_with));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{cameraIntent});
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);
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();
this.stories.addAll(
this.xmppConnectionService.getStories().stream()
.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()));
if (this.stories.isEmpty()) {
binding.storiesList.setVisibility(View.GONE);
} else {
binding.storiesList.setVisibility(View.VISIBLE);
}
this.storyAdapter.notifyDataSetChanged();
}
@Override
public void onStoriesUpdate() {
runOnUiThread(this::refresh);
}
}

View file

@ -8,6 +8,9 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.List;
import eu.siacs.conversations.R;
@ -66,6 +69,9 @@ public class StoryAdapter extends RecyclerView.Adapter<StoryAdapter.StoryViewHol
holder.storyTitle.setText(jid.asBareJid().toString());
holder.storyImage.setImageResource(R.drawable.ic_person_black_48dp);
}
Glide.with(activity).load(story.getUrl()).into(holder.storyPreview);
final Account finalStoryAccount = storyAccount;
holder.itemView.setOnClickListener(v -> {
Intent intent = new Intent(activity, StoryViewActivity.class);
@ -101,11 +107,13 @@ public class StoryAdapter extends RecyclerView.Adapter<StoryAdapter.StoryViewHol
final ImageView storyImage;
final TextView storyTitle;
final ImageView storyPreview;
StoryViewHolder(@NonNull View itemView) {
super(itemView);
storyImage = itemView.findViewById(R.id.story_image);
storyTitle = itemView.findViewById(R.id.story_title);
storyPreview = itemView.findViewById(R.id.story_preview);
}
}
}

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,61 @@
<?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" />
</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>

View file

@ -10,14 +10,6 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/stories_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<RelativeLayout
android:id="@+id/overview_snackbar"
android:layout_width="fill_parent"
@ -65,15 +57,6 @@
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_story"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="16dp"
android:layout_marginBottom="92dp"
app:srcCompat="@drawable/outline_add_a_photo_24" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"

View file

@ -1,23 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
android:padding="12dp"
android:minHeight="?android:attr/listPreferredItemHeight">
<eu.siacs.conversations.ui.widget.AvatarView
android:id="@+id/story_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/story_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:lines="1"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_toStartOf="@id/story_preview"
android:layout_toEndOf="@id/story_image"
android:ellipsize="end"
android:textSize="12sp" />
android:maxLines="1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" />
</LinearLayout>
<ImageView
android:id="@+id/story_preview"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:scaleType="centerCrop" />
</RelativeLayout>

View file

@ -1,6 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:orderInCategory="5"
android:title="@string/search_messages"

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View file

@ -8,6 +8,10 @@
android:id="@+id/contactslist"
android:icon="?attr/ic_group_unselected"
android:title="@string/contacts"/>
<item
android:id="@+id/stories"
android:icon="?attr/ic_stories_unselected"
android:title="@string/stories"/>
<item
android:id="@+id/manageaccounts"
android:icon="?attr/ic_accounts_selected"

View file

@ -8,6 +8,10 @@
android:id="@+id/contactslist"
android:icon="?attr/ic_group_unselected"
android:title="@string/contacts"/>
<item
android:id="@+id/stories"
android:icon="?attr/ic_stories_unselected"
android:title="@string/stories"/>
<item
android:id="@+id/manageaccounts"
android:icon="?attr/ic_account"

View file

@ -8,6 +8,10 @@
android:id="@+id/contactslist"
android:icon="?attr/ic_group_selected"
android:title="@string/contacts"/>
<item
android:id="@+id/stories"
android:icon="?attr/ic_stories_unselected"
android:title="@string/stories"/>
<item
android:id="@+id/manageaccounts"
android:icon="?attr/ic_account"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/chats"
android:icon="?attr/ic_chat_unselected"
android:title="@string/chats"/>
<item
android:id="@+id/contactslist"
android:icon="?attr/ic_group_unselected"
android:title="@string/contacts"/>
<item
android:id="@+id/stories"
android:icon="?attr/ic_stories_selected"
android:title="@string/stories"/>
<item
android:id="@+id/manageaccounts"
android:icon="?attr/ic_account"
android:title="@string/accounts"/>
</menu>

View file

@ -8,13 +8,6 @@
android:title="@string/search_messages"
android:visible="@bool/show_individual_search_options"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_add_story"
android:icon="@drawable/outline_add_a_photo_24"
android:orderInCategory="2"
android:title="@string/add_story"
android:visible="false"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_note_to_self"
android:orderInCategory="80"
@ -26,11 +19,6 @@
android:orderInCategory="89"
android:title="@string/invite_to_app"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_toggle_stories"
android:orderInCategory="90"
android:title="@string/show_stories"
app:showAsAction="never" />
<item
android:id="@+id/action_accounts"
android:orderInCategory="91"
@ -41,6 +29,11 @@
android:orderInCategory="91"
android:title="@string/action_account"
app:showAsAction="never" />
<item
android:id="@+id/action_stories"
android:orderInCategory="92"
android:title="@string/stories"
app:showAsAction="never" />
<item
android:id="@+id/action_privacy_policy"
android:orderInCategory="98"

View file

@ -36,6 +36,8 @@
<item name="ic_accounts_selected" type="reference">@drawable/accounts_selected_white_24</item>
<item name="ic_group_unselected" type="reference">@drawable/outline_group_white_24</item>
<item name="ic_group_selected" type="reference">@drawable/ic_group_selected_white_24</item>
<item name="ic_stories_unselected" type="reference">@drawable/stories_unselected_white_24</item>
<item name="ic_stories_selected" type="reference">@drawable/stories_selected_white_24</item>
<item name="chat_bg">@drawable/bg_dark_blue</item>
</style>

View file

@ -61,6 +61,8 @@
<item name="ic_accounts_selected" type="reference">@drawable/accounts_selected_black_24</item>
<item name="ic_group_unselected" type="reference">@drawable/outline_group_black_24dp</item>
<item name="ic_group_selected" type="reference">@drawable/ic_group_selected_black_24</item>
<item name="ic_stories_unselected" type="reference">@drawable/stories_unselected_black_24</item>
<item name="ic_stories_selected" type="reference">@drawable/stories_selected_black_24</item>
<item name="chat_bg">@drawable/bg_light_blue</item>
</style>

View file

@ -232,6 +232,13 @@ public class ManageAccountActivity extends XmppActivity implements XmppConnectio
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.manageaccounts -> {
return true;
}

View file

@ -10,6 +10,8 @@
<attr name="ic_accounts_selected" format="reference" />
<attr name="ic_group_unselected" format="reference" />
<attr name="ic_group_selected" format="reference" />
<attr name="ic_stories_unselected" format="reference" />
<attr name="ic_stories_selected" format="reference" />
<attr name="chat_bg" format="reference|color" />
<declare-styleable name="ReadMoreTextView">