diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 3d3ff0719..01117522f 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -201,7 +201,7 @@ android:launchMode="singleTask" android:minWidth="300dp" android:minHeight="300dp" - android:windowSoftInputMode="stateHidden" /> + android:windowSoftInputMode="adjustPan|stateHidden" /> finish()); setTitle(R.string.add_reaction_title); - binding.emojiPicker.setOnEmojiPickedListener( + binding.reactionPicker.setOnEmojiPickedListener( emojiViewItem -> addReaction(emojiViewItem.getEmoji())); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 54ec84df3..9b0033874 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -6,6 +6,7 @@ import static android.view.View.VISIBLE; import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; +import static eu.siacs.conversations.utils.Compatibility.hasStoragePermission; import static eu.siacs.conversations.utils.PermissionUtils.allGranted; import static eu.siacs.conversations.utils.PermissionUtils.audioGranted; import static eu.siacs.conversations.utils.PermissionUtils.cameraGranted; @@ -30,6 +31,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.graphics.Color; +import android.graphics.Typeface; import android.icu.util.Calendar; import android.icu.util.TimeZone; import android.media.MediaRecorder; @@ -62,6 +64,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; @@ -73,9 +76,11 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.CheckBox; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.PopupWindow; +import android.widget.RelativeLayout; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; @@ -85,13 +90,17 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.ColorUtils; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.databinding.DataBindingUtil; import androidx.documentfile.provider.DocumentFile; +import androidx.emoji2.emojipicker.EmojiPickerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; @@ -100,6 +109,9 @@ import com.bumptech.glide.Glide; import de.monocles.chat.BobTransfer; import de.monocles.chat.EmojiSearch; +import de.monocles.chat.EmojiSearchOld; +import de.monocles.chat.GifsAdapter; +import de.monocles.chat.KeyboardHeightProvider; import de.monocles.chat.WebxdcPage; import de.monocles.chat.WebxdcStore; @@ -130,6 +142,8 @@ import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.AbstractMap; import java.util.ArrayList; @@ -151,6 +165,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -307,6 +322,15 @@ public class ConversationFragment extends XmppFragment private int identiconWidth = -1; private File savingAsSticker = null; private EmojiSearch emojiSearch = null; + File dirStickers = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "Stickers"); + //Gifspaths + private File[] files; + private String[] filesPaths; + private String[] filesNames; + File dirGifs = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "Stickers"); //TODO: Change this to dedicated GIFs folder later + + private KeyboardHeightProvider.KeyboardHeightListener keyboardHeightListener = null; + private KeyboardHeightProvider keyboardHeightProvider = null; protected OnClickListener clickToVerify = new OnClickListener() { @Override @@ -1493,6 +1517,7 @@ public class ConversationFragment extends XmppFragment setHasOptionsMenu(true); activity.getOnBackPressedDispatcher().addCallback(this, backPressedLeaveSingleThread); activity.getOnBackPressedDispatcher().addCallback(this, backPressedLeaveVoiceRecorder); + activity.getOnBackPressedDispatcher().addCallback(this, backPressedLeaveEmojiPicker); oldOrientation = activity.getRequestedOrientation(); } @@ -1579,6 +1604,10 @@ public class ConversationFragment extends XmppFragment DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); binding.getRoot().setOnClickListener(null); // TODO why the fuck did we do this? + backPressedLeaveEmojiPicker.setEnabled(binding.emojisStickerLayout.getHeight() > 100); + LoadStickers(); + LoadGifs(); + binding.textinput.setOnEditorActionListener(mEditorActionListener); binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener); DisplayMetrics displayMetrics = new DisplayMetrics(); @@ -1598,6 +1627,11 @@ public class ConversationFragment extends XmppFragment binding.requestVoice.setVisibility(View.GONE); Toast.makeText(activity, "Your request has been sent to the moderators", Toast.LENGTH_SHORT).show(); }); + binding.emojiButton.setOnClickListener(this.memojiButtonListener); + binding.emojisButton.setOnClickListener(this.memojisButtonListener); + binding.stickersButton.setOnClickListener(this.mstickersButtonListener); + binding.gifsButton.setOnClickListener(this.mgifsButtonListener); + binding.keyboardButton.setOnClickListener(this.mkeyboardButtonListener); binding.recordVoiceButton.setOnClickListener(this.mRecordVoiceButtonListener); binding.timer.setOnClickListener(this.mTimerClickListener); binding.takePictureButton.setOnClickListener(this.mtakePictureButtonListener); @@ -2393,6 +2427,14 @@ public class ConversationFragment extends XmppFragment updateThreadFromLastMessage(); return true; } + if (binding.emojisStickerLayout.getHeight() > 100){ + LinearLayout emojipickerview = binding.emojisStickerLayout; + ViewGroup.LayoutParams params = emojipickerview.getLayoutParams(); + params.height = 0; + emojipickerview.setLayoutParams(params); + hideSoftKeyboard(activity); + return false; + } if (binding.recordingVoiceActivity.getVisibility()==VISIBLE){ mHandler.removeCallbacks(mTickExecutor); stopRecording(false); @@ -3566,7 +3608,10 @@ public class ConversationFragment extends XmppFragment activity.onConversationArchived(this.conversation); return false; } - + updateinputfield(); + if (activity != null) { + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + } final var cursord = activity.getDrawable(R.drawable.cursor_on_tertiary_container); if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) { final var bg = MaterialColors.getColor(binding.textinput, com.google.android.material.R.attr.colorSurface); @@ -5335,4 +5380,378 @@ public class ConversationFragment extends XmppFragment } } + public void updateinputfield() { + LinearLayout emojipickerview = binding.emojisStickerLayout; + ViewGroup.LayoutParams params = emojipickerview.getLayoutParams(); + Fragment secondaryFragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment); + if (Build.VERSION.SDK_INT > 29) { + ViewCompat.setOnApplyWindowInsetsListener(activity.getWindow().getDecorView(), (v, insets) -> { + boolean isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + int keyboardHeight = 0; + if (activity != null && ViewConfiguration.get(activity).hasPermanentMenuKey()) { + keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom - insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom - 25; + } else if (activity != null) { + keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom - insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom - 25; + } + if (keyboardHeight > 100 && !(secondaryFragment instanceof ConversationFragment)) { + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + params.height = keyboardHeight; + emojipickerview.setLayoutParams(params); + } else if (keyboardHeight > 100) { + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + params.height = keyboardHeight - 142; + emojipickerview.setLayoutParams(params); + } else if (binding.emojiButton.getVisibility() == VISIBLE) { + binding.keyboardButton.setVisibility(GONE); + params.height = 0; + emojipickerview.setLayoutParams(params); + } else if (binding.keyboardButton.getVisibility() == VISIBLE && keyboardHeight == 0) { + binding.emojiButton.setVisibility(GONE); + params.height = 800; + emojipickerview.setLayoutParams(params); + } else if (binding.keyboardButton.getVisibility() == VISIBLE && keyboardHeight > 100) { + binding.emojiButton.setVisibility(GONE); + params.height = keyboardHeight; + emojipickerview.setLayoutParams(params); + } + return ViewCompat.onApplyWindowInsets(v, insets); + }); + } else { + if (keyboardHeightProvider != null) { + return; + } + RelativeLayout llRoot = binding.conversationsFragment; //The root layout (Linear, Relative, Contraint, etc...) + keyboardHeightListener = (int keyboardHeight, boolean keyboardOpen, boolean isLandscape) -> { + Log.i("keyboard listener", "keyboardHeight: " + keyboardHeight + " keyboardOpen: " + keyboardOpen + " isLandscape: " + isLandscape); + if (keyboardOpen && !(secondaryFragment instanceof ConversationFragment)) { + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + params.height = keyboardHeight - 25; + emojipickerview.setLayoutParams(params); + } else if (keyboardOpen) { + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + params.height = keyboardHeight - 150; + emojipickerview.setLayoutParams(params); + } else if (binding.emojiButton.getVisibility() == VISIBLE) { + binding.keyboardButton.setVisibility(GONE); + params.height = 0; + emojipickerview.setLayoutParams(params); + } else if (binding.keyboardButton.getVisibility() == VISIBLE && keyboardHeight == 0) { + binding.emojiButton.setVisibility(GONE); + params.height = 600; + emojipickerview.setLayoutParams(params); + } else if (binding.keyboardButton.getVisibility() == VISIBLE && keyboardHeight > 100) { + binding.emojiButton.setVisibility(GONE); + params.height = keyboardHeight; + emojipickerview.setLayoutParams(params); + } + }; + keyboardHeightProvider = new KeyboardHeightProvider(activity, activity.getWindowManager(), llRoot, keyboardHeightListener); + } + } + + private final OnClickListener memojiButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (binding.emojiButton.getVisibility() == VISIBLE && binding.emojisStickerLayout.getHeight() > 100) { + binding.emojiButton.setVisibility(GONE); + binding.keyboardButton.setVisibility(VISIBLE); + hideSoftKeyboard(activity); + EmojiPickerView emojiPickerView = binding.emojiPicker; + backPressedLeaveEmojiPicker.setEnabled(true); + binding.textinput.requestFocus(); + emojiPickerView.setOnEmojiPickedListener(emojiViewItem -> { + int start = binding.textinput.getSelectionStart(); //this is to get the the cursor position + binding.textinput.getText().insert(start, emojiViewItem.getEmoji()); //this will get the text and insert the emoji into the current position + }); + + if (binding.emojiPicker.getVisibility() == VISIBLE) { + binding.emojisButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.emojisButton.setTypeface(null, Typeface.BOLD); + } else { + binding.emojisButton.setBackgroundColor(0); + binding.emojisButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.stickersview.getVisibility() == VISIBLE) { + binding.stickersButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.stickersButton.setTypeface(null, Typeface.BOLD); + } else { + binding.stickersButton.setBackgroundColor(0); + binding.stickersButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.gifsview.getVisibility() == VISIBLE) { + binding.gifsButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.gifsButton.setTypeface(null, Typeface.BOLD); + } else { + binding.gifsButton.setBackgroundColor(0); + binding.gifsButton.setTypeface(null, Typeface.NORMAL); + } + } else if (binding.emojiButton.getVisibility() == VISIBLE && binding.emojisStickerLayout.getHeight() < 100) { + LinearLayout emojipickerview = binding.emojisStickerLayout; + ViewGroup.LayoutParams params = emojipickerview.getLayoutParams(); + params.height = 800; + emojipickerview.setLayoutParams(params); + binding.emojiButton.setVisibility(GONE); + binding.keyboardButton.setVisibility(VISIBLE); + hideSoftKeyboard(activity); + EmojiPickerView emojiPickerView = binding.emojiPicker; + backPressedLeaveEmojiPicker.setEnabled(true); + binding.textinput.requestFocus(); + emojiPickerView.setOnEmojiPickedListener(emojiViewItem -> { + int start = binding.textinput.getSelectionStart(); //this is to get the the cursor position + binding.textinput.getText().insert(start, emojiViewItem.getEmoji()); //this will get the text and insert the emoji into the current position + }); + + if (binding.emojiPicker.getVisibility() == VISIBLE) { + binding.emojisButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.emojisButton.setTypeface(null, Typeface.BOLD); + } else { + binding.emojisButton.setBackgroundColor(0); + binding.emojisButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.stickersview.getVisibility() == VISIBLE) { + binding.stickersButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.stickersButton.setTypeface(null, Typeface.BOLD); + } else { + binding.stickersButton.setBackgroundColor(0); + binding.stickersButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.gifsview.getVisibility() == VISIBLE) { + binding.gifsButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.gifsButton.setTypeface(null, Typeface.BOLD); + } else { + binding.gifsButton.setBackgroundColor(0); + binding.gifsButton.setTypeface(null, Typeface.NORMAL); + } + } + } + }; + + private final OnClickListener memojisButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + binding.emojiPicker.setVisibility(VISIBLE); + binding.stickersview.setVisibility(GONE); + binding.gifsview.setVisibility(GONE); + EmojiPickerView emojiPickerView = binding.emojiPicker; + backPressedLeaveEmojiPicker.setEnabled(true); + binding.textinput.requestFocus(); + emojiPickerView.setOnEmojiPickedListener(emojiViewItem -> { + int start = binding.textinput.getSelectionStart(); //this is to get the the cursor position + binding.textinput.getText().insert(start, emojiViewItem.getEmoji()); //this will get the text and insert the emoji into the current position + }); + + if (binding.emojiPicker.getVisibility() == VISIBLE) { + binding.emojisButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.emojisButton.setTypeface(null, Typeface.BOLD); + } else { + binding.emojisButton.setBackgroundColor(0); + binding.emojisButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.stickersview.getVisibility() == VISIBLE) { + binding.stickersButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.stickersButton.setTypeface(null, Typeface.BOLD); + } else { + binding.stickersButton.setBackgroundColor(0); + binding.stickersButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.gifsview.getVisibility() == VISIBLE) { + binding.gifsButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.gifsButton.setTypeface(null, Typeface.BOLD); + } else { + binding.gifsButton.setBackgroundColor(0); + binding.gifsButton.setTypeface(null, Typeface.NORMAL); + } + } + }; + + private final OnClickListener mstickersButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + binding.emojiPicker.setVisibility(GONE); + binding.stickersview.setVisibility(VISIBLE); + binding.gifsview.setVisibility(GONE); + backPressedLeaveEmojiPicker.setEnabled(true); + binding.textinput.requestFocus(); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isEmpty(dirStickers.toPath())) { + Toast.makeText(activity, R.string.update_default_stickers, Toast.LENGTH_LONG).show(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + if (binding.emojiPicker.getVisibility() == VISIBLE) { + binding.emojisButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.emojisButton.setTypeface(null, Typeface.BOLD); + } else { + binding.emojisButton.setBackgroundColor(0); + binding.emojisButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.stickersview.getVisibility() == VISIBLE) { + binding.stickersButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.stickersButton.setTypeface(null, Typeface.BOLD); + } else { + binding.stickersButton.setBackgroundColor(0); + binding.stickersButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.gifsview.getVisibility() == VISIBLE) { + binding.gifsButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.gifsButton.setTypeface(null, Typeface.BOLD); + } else { + binding.gifsButton.setBackgroundColor(0); + binding.gifsButton.setTypeface(null, Typeface.NORMAL); + } + } + }; + + public boolean isEmpty(Path path) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Files.isDirectory(path)) { + try (Stream entries = Files.list(path)) { + return !entries.findFirst().isPresent(); + } + } + } + return false; + } + + private final OnClickListener mgifsButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + binding.emojiPicker.setVisibility(GONE); + binding.stickersview.setVisibility(GONE); + binding.gifsview.setVisibility(VISIBLE); + backPressedLeaveEmojiPicker.setEnabled(true); + binding.textinput.requestFocus(); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isEmpty(dirGifs.toPath())) { + Toast.makeText(activity, R.string.copy_GIFs_to_GIFs_folder, Toast.LENGTH_LONG).show(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + if (binding.emojiPicker.getVisibility() == VISIBLE) { + binding.emojisButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.emojisButton.setTypeface(null, Typeface.BOLD); + } else { + binding.emojisButton.setBackgroundColor(0); + binding.emojisButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.stickersview.getVisibility() == VISIBLE) { + binding.stickersButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.stickersButton.setTypeface(null, Typeface.BOLD); + } else { + binding.stickersButton.setBackgroundColor(0); + binding.stickersButton.setTypeface(null, Typeface.NORMAL); + } + if (binding.gifsview.getVisibility() == VISIBLE) { + binding.gifsButton.setBackground(ContextCompat.getDrawable(activity, R.drawable.selector_bubble)); + binding.gifsButton.setTypeface(null, Typeface.BOLD); + } else { + binding.gifsButton.setBackgroundColor(0); + binding.gifsButton.setTypeface(null, Typeface.NORMAL); + } + } + }; + + private final OnClickListener mkeyboardButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (binding.keyboardButton.getVisibility() == VISIBLE) { + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + binding.textinput.requestFocus(); + inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT); + } + } + } + }; + + private final OnBackPressedCallback backPressedLeaveEmojiPicker = new OnBackPressedCallback(false) { + @Override + public void handleOnBackPressed() { + if (binding.emojisStickerLayout.getHeight() > 100) { + LinearLayout emojipickerview = binding.emojisStickerLayout; + ViewGroup.LayoutParams params = emojipickerview.getLayoutParams(); + params.height = 0; + emojipickerview.setLayoutParams(params); + binding.keyboardButton.setVisibility(GONE); + binding.emojiButton.setVisibility(VISIBLE); + } + this.setEnabled(false); + refresh(); + } + }; + + public void LoadStickers() { + final Pattern lastColonPattern = Pattern.compile(""); + binding.stickersview.setOnItemClickListener((parent, view, position, id) -> { + EmojiSearchOld.EmojiSearchAdapter adapter = ((EmojiSearchOld.EmojiSearchAdapter) binding.stickersview.getAdapter()); + Editable toInsert = adapter.getItem(position).toInsert(); + toInsert.append(" "); + Editable s = binding.textinput.getText(); + + Matcher lastColonMatcher = lastColonPattern.matcher(s); + int lastColon = 0; + while(lastColonMatcher.find()) lastColon = lastColonMatcher.end(); + if (lastColon >= 0) { + int start = binding.textinput.getSelectionStart(); //this is to get the the cursor position + binding.textinput.getText().insert(start, toInsert); //this will get the text and insert the emoji into the current position + } + }); + + setupEmojiSearch(); + } + + public void LoadGifs() { + if (!hasStoragePermission(activity)) return; + // Load and show GIFs + if (!dirGifs.exists()) { + dirGifs.mkdir(); + } + if (dirGifs.listFiles() != null) { + if (dirGifs.isDirectory() && dirGifs.listFiles() != null) { + files = dirGifs.listFiles(); + filesPaths = new String[files.length]; + filesNames = new String[files.length]; + for (int i = 0; i < files.length; i++) { + filesPaths[i] = files[i].getAbsolutePath(); + filesNames[i] = files[i].getName(); + } + } + } + de.monocles.chat.GridView GifsGrid = binding.gifsview; // init GridView + // Create an object of CustomAdapter and set Adapter to GirdView + GifsGrid.setAdapter(new GifsAdapter(activity, filesNames, filesPaths)); + // implement setOnItemClickListener event on GridView + GifsGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (activity == null) return; + String filePath = filesPaths[position]; + mediaPreviewAdapter.addMediaPreviews(Attachment.of(activity, Uri.fromFile(new File(filePath)), Attachment.Type.IMAGE)); + toggleInputMethod(); + } + }); + + GifsGrid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (activity != null && filesPaths[position] != null) { + File file = new File(filesPaths[position]); + if (file.delete()) { + Toast.makeText(activity, R.string.gif_deleted, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(activity, R.string.failed_to_delete_gif, Toast.LENGTH_LONG).show(); + } + } + return true; + } + }); + } } diff --git a/src/main/res/drawable/outline_emoji_emotions_24.xml b/src/main/res/drawable/outline_emoji_emotions_24.xml new file mode 100644 index 000000000..8d593eae6 --- /dev/null +++ b/src/main/res/drawable/outline_emoji_emotions_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/res/drawable/rounded_broken_image_24.xml b/src/main/res/drawable/rounded_broken_image_24.xml new file mode 100644 index 000000000..0e24c3d21 --- /dev/null +++ b/src/main/res/drawable/rounded_broken_image_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/res/drawable/rounded_keyboard_24.xml b/src/main/res/drawable/rounded_keyboard_24.xml new file mode 100644 index 000000000..fa1133f1e --- /dev/null +++ b/src/main/res/drawable/rounded_keyboard_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/res/drawable/selector_bubble.xml b/src/main/res/drawable/selector_bubble.xml new file mode 100644 index 000000000..7c4d08340 --- /dev/null +++ b/src/main/res/drawable/selector_bubble.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_add_reaction.xml b/src/main/res/layout/activity_add_reaction.xml index 0a963b02c..96e73d17b 100644 --- a/src/main/res/layout/activity_add_reaction.xml +++ b/src/main/res/layout/activity_add_reaction.xml @@ -18,7 +18,7 @@ diff --git a/src/main/res/layout/activity_gridview_gifs.xml b/src/main/res/layout/activity_gridview_gifs.xml new file mode 100644 index 000000000..bbd27f426 --- /dev/null +++ b/src/main/res/layout/activity_gridview_gifs.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 9316de1c3..445802027 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -3,8 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:layout_height="match_parent" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Related chats Add reaction More reactions + Choose emojis + Show keyboard + Emojis + Stickers + GIFs + Update default stickers + Copy GIFs to GIFs folder + GIF deleted + Failed to delete GIF diff --git a/src/monocleschat/java/de/monocles/chat/EmojiSearchOld.java b/src/monocleschat/java/de/monocles/chat/EmojiSearchOld.java new file mode 100644 index 000000000..e24202e8a --- /dev/null +++ b/src/monocleschat/java/de/monocles/chat/EmojiSearchOld.java @@ -0,0 +1,237 @@ +package de.monocles.chat; + +import android.app.Activity; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import androidx.databinding.DataBindingUtil; + +import com.google.common.collect.Lists; +import com.google.common.io.CharStreams; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.Comparable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.TreeSet; + +import me.xdrop.fuzzywuzzy.FuzzySearch; +import me.xdrop.fuzzywuzzy.model.BoundExtractedResult; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.EmojiSearchRowBinding; +import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; + +public class EmojiSearchOld { + protected final Set emoji = new TreeSet<>(); + + public EmojiSearchOld(Context context) { + /* TODO: No emoji search needed since there already is an emoji keyboard + try { + final JSONArray data = new JSONArray(CharStreams.toString(new InputStreamReader(context.getResources().openRawResource(R.raw.emoji), "UTF-8"))); + for (int i = 0; i < data.length(); i++) { + emoji.add(new Emoji(data.getJSONObject(i))); + } + } catch (final JSONException | IOException e) { + throw new IllegalStateException("emoji.json invalid: " + e); + } + */ + } + + public synchronized void addEmoji(final Emoji one) { + emoji.add(one); + } + + public synchronized List find(final String q) { + final ResultPQ pq = new ResultPQ(); + for (Emoji e : emoji) { + if (e.emoticonMatch(q)) { + pq.addTopK(e, 999999, 999); + } + int shortcodeScore = e.shortcodes.isEmpty() ? 0 : Collections.max(Lists.transform(e.shortcodes, (shortcode) -> FuzzySearch.ratio(q, shortcode))); + int tagScore = e.tags.isEmpty() ? 0 : Collections.max(Lists.transform(e.tags, (tag) -> FuzzySearch.ratio(q, tag))) - 2; + pq.addTopK(e, Math.max(shortcodeScore, tagScore), 999); + } + + for (BoundExtractedResult r : new ArrayList<>(pq)) { + for (Emoji e : emoji) { + if (e.shortcodeMatch(r.getReferent().uniquePart())) { + // hack see https://stackoverflow.com/questions/76880072/imagespan-with-emojicompat + e.shortcodes.clear(); + e.shortcodes.addAll(r.getReferent().shortcodes); + + pq.addTopK(e, r.getScore() - 1, 999); + } + } + } + + List result = new ArrayList<>(); + for (int i = 0; i < 999; i++) { + BoundExtractedResult e = pq.poll(); + if (e != null) result.add(e.getReferent()); + } + Collections.reverse(result); + return result; + } + + public EmojiSearchAdapter makeAdapter(Activity context) { + return new EmojiSearchAdapter(context); + } + + public static class ResultPQ extends PriorityQueue> { + public void addTopK(Emoji e, int score, int k) { + BoundExtractedResult r = new BoundExtractedResult(e, null, score, 0); + if (size() < k) { + add(r); + } else if (r.compareTo(peek()) > 0) { + poll(); + add(r); + } + } + } + + public static class Emoji implements Comparable { + protected final String unicode; + protected final int order; + protected final List tags = new ArrayList<>(); + protected final List emoticon = new ArrayList<>(); + protected final List shortcodes = new ArrayList<>(); + + public Emoji(final String unicode, final int order) { + this.unicode = unicode; + this.order = order; + } + + public Emoji(JSONObject o) throws JSONException { + unicode = o.getString("unicode"); + order = o.getInt("order"); + final JSONArray rawTags = o.getJSONArray("tags"); + for (int i = 0; i < rawTags.length(); i++) { + tags.add(rawTags.getString(i)); + } + final JSONArray rawEmoticon = o.getJSONArray("emoticon"); + for (int i = 0; i < rawEmoticon.length(); i++) { + emoticon.add(rawEmoticon.getString(i)); + } + final JSONArray rawShortcodes = o.getJSONArray("shortcodes"); + for (int i = 0; i < rawShortcodes.length(); i++) { + shortcodes.add(rawShortcodes.getString(i)); + } + } + + public boolean emoticonMatch(final String q) { + for (final String emote : emoticon) { + if (emote.equals(q) || emote.equals(":" + q)) return true; + } + + return false; + } + + public boolean shortcodeMatch(final String q) { + for (final String shortcode : shortcodes) { + if (shortcode.equals(q)) return true; + } + + return false; + } + + public SpannableStringBuilder toInsert() { + return new SpannableStringBuilder(unicode); + } + + public String uniquePart() { + return unicode; + } + + @Override + public int compareTo(Emoji o) { + if (equals(o)) return 0; + if (order == o.order) return uniquePart().compareTo(o.uniquePart()); + return order - o.order; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Emoji)) return false; + + return uniquePart().equals(((Emoji) o).uniquePart()); + } + } + + public static class CustomEmoji extends Emoji { + protected final String source; + protected final Drawable icon; + + public CustomEmoji(final String shortcode, final String source, final Drawable icon, final String tag) { + super(null, 999); + shortcodes.add(shortcode); + if (tag != null) tags.add(tag); + this.source = source; + this.icon = icon; + if (icon == null) { + throw new IllegalArgumentException("icon must not be null"); + } + } + + public SpannableStringBuilder toInsert() { + SpannableStringBuilder builder = new SpannableStringBuilder(":" + shortcodes.get(0) + ":"); + builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return builder; + } + + @Override + public String uniquePart() { + return source; + } + } + + public class EmojiSearchAdapter extends ArrayAdapter { + ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter"); + + public EmojiSearchAdapter(Activity context) { + super(context, 0); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + EmojiSearchRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.emoji_search_row, parent, false); + if (getItem(position) instanceof CustomEmoji) { + binding.nonunicode.setText(getItem(position).toInsert()); + binding.nonunicode.setVisibility(View.VISIBLE); + binding.unicode.setVisibility(View.GONE); + } else { + binding.unicode.setText(getItem(position).toInsert()); + binding.unicode.setVisibility(View.VISIBLE); + binding.nonunicode.setVisibility(View.GONE); + } + binding.shortcode.setText(getItem(position).shortcodes.get(0)); + return binding.getRoot(); + } + + public void search(final String q) { + executor.execute(() -> { + final List results = find(q); + ((Activity) getContext()).runOnUiThread(() -> { + clear(); + addAll(results); + notifyDataSetChanged(); + }); + }); + } + } +} diff --git a/src/monocleschat/java/de/monocles/chat/GifsAdapter.java b/src/monocleschat/java/de/monocles/chat/GifsAdapter.java new file mode 100644 index 000000000..0710258b2 --- /dev/null +++ b/src/monocleschat/java/de/monocles/chat/GifsAdapter.java @@ -0,0 +1,58 @@ +package de.monocles.chat; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; + +import eu.siacs.conversations.R; + +public class GifsAdapter extends BaseAdapter { + private Context ctx; + private final String[] filesNames; + private final String[] filesPaths; + + public GifsAdapter(Context ctx, String[] filesNames, String[] filesPaths) { + this.ctx = ctx; + this.filesNames = filesNames; + this.filesPaths = filesPaths; + } + + @Override + public int getCount() { + if (filesNames != null) { + return filesNames.length; + } else { + return 0; + } + } + + @Override + public Object getItem(int pos) { + return pos; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View v; + if (convertView == null) { // if it's not recycled, initialize some attributes + LayoutInflater inflater = (LayoutInflater) ctx.getSystemService( Context.LAYOUT_INFLATER_SERVICE ); + v = inflater.inflate(R.layout.activity_gridview_gifs, parent, false); + } else { + v = (View) convertView; + } + ImageView image = (ImageView)v.findViewById(R.id.grid_item); + Glide.with(ctx).load(filesPaths[position]).into(image); + return v; + } + + +} \ No newline at end of file diff --git a/src/monocleschat/java/de/monocles/chat/KeyboardHeightProvider.java b/src/monocleschat/java/de/monocles/chat/KeyboardHeightProvider.java new file mode 100644 index 000000000..dc5f84f58 --- /dev/null +++ b/src/monocleschat/java/de/monocles/chat/KeyboardHeightProvider.java @@ -0,0 +1,69 @@ +package de.monocles.chat; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +public class KeyboardHeightProvider extends PopupWindow { + LinearLayout popupView; + ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener; + public KeyboardHeightProvider(Context context, WindowManager windowManager, View parentView, KeyboardHeightListener listener) { + super(context); + + popupView = new LinearLayout(context); + popupView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + globalLayoutListener = () -> { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + + Rect rect = new Rect(); + popupView.getWindowVisibleDisplayFrame(rect); + + int keyboardHeight = metrics.heightPixels - (rect.bottom - rect.top); + int resourceID = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceID > 0) { + keyboardHeight -= context.getResources().getDimensionPixelSize(resourceID); + } + if (keyboardHeight < 100) { + keyboardHeight = 0; + } + boolean isLandscape = metrics.widthPixels > metrics.heightPixels; + boolean keyboardOpen = keyboardHeight > 0; + if (listener != null) { + listener.onKeyboardHeightChanged(keyboardHeight, keyboardOpen, isLandscape); + } + }; + popupView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); + + setContentView(popupView); + + setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + setWidth(0); + setHeight(ViewGroup.LayoutParams.MATCH_PARENT); + setBackgroundDrawable(new ColorDrawable(0)); + + parentView.post(() -> showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0)); + } + + @Override + public void dismiss() { + if (globalLayoutListener != null) { + popupView.getViewTreeObserver().removeOnGlobalLayoutListener(globalLayoutListener); + globalLayoutListener = null; + } + super.dismiss(); + } + + public interface KeyboardHeightListener { + void onKeyboardHeightChanged(int keyboardHeight, boolean keyboardOpen, boolean isLandscape); + } +} \ No newline at end of file