diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 56bad8a2d7..4f9c239429 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -15,3 +15,11 @@ jobs: project: Issue triage column: Incoming repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + triage-new-issues: + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/vector-im/projects/91 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/CHANGES.md b/CHANGES.md index 0fa9d9bb15..53ddf8a4ef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,16 @@ +Changes in Element v1.6.5 (2023-07-25) +====================================== + +Bugfixes 🐛 +---------- + - Fix several crashes observed when the device cannot reach the homeserver ([#8578](https://github.com/vector-im/element-android/issues/8578)) + +Other changes +------------- + - Update MSC3912 implementation: Redaction of related events ([#8481](https://github.com/vector-im/element-android/issues/8481)) + - Include some source code in our project to remove our dependency to artifact hosted by bintray (Jcenter). ([#8556](https://github.com/vector-im/element-android/issues/8556)) + + Changes in Element v1.6.3 (2023-06-27) ====================================== diff --git a/build.gradle b/build.gradle index 30692d0853..03b3df01ad 100644 --- a/build.gradle +++ b/build.gradle @@ -104,16 +104,6 @@ allprojects { groups.google.group.each { includeGroup it } } } - //noinspection JcenterRepositoryObsolete - // Do not use `jcenter`, it prevents Dependabot from working properly - maven { - url 'https://jcenter.bintray.com' - content { - groups.jcenter.regex.each { includeGroupByRegex it } - groups.jcenter.group.each { includeGroup it } - } - } - maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' content { @@ -121,7 +111,6 @@ allprojects { groups.mavenSnapshots.group.each { includeGroup it } } } - } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 01e1326e45..25b09c436b 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -117,6 +117,7 @@ ext.groups = [ 'com.linkedin.dexmaker', 'com.mapbox.mapboxsdk', 'com.nulab-inc', + 'com.otaliastudios', 'com.otaliastudios.opengl', 'com.parse.bolts', 'com.pinterest', @@ -236,18 +237,4 @@ ext.groups = [ 'xml-apis', ] ], - jcenter : [ - regex: [ - ], - group: [ - 'com.amulyakhare', - 'com.otaliastudios', - 'com.yqritc', - // https://github.com/cmelchior/realmfieldnameshelper/issues/42 - 'dk.ilios', - 'im.dlg', - 'me.dm7.barcodescanner', - 'me.gujun.android', - ] - ] ] diff --git a/fastlane/metadata/android/en-US/changelogs/40106050.txt b/fastlane/metadata/android/en-US/changelogs/40106050.txt new file mode 100644 index 0000000000..49eb8e5d5e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106050.txt @@ -0,0 +1,2 @@ +Main changes in this version: corrective release. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hy/short_description.txt b/fastlane/metadata/android/hy/short_description.txt new file mode 100644 index 0000000000..3b02739117 --- /dev/null +++ b/fastlane/metadata/android/hy/short_description.txt @@ -0,0 +1 @@ +Խմբային մեսինջեր - գաղտնագրված շփում, խմբային չատեր և վիդեո զանգեր diff --git a/fastlane/metadata/android/hy/title.txt b/fastlane/metadata/android/hy/title.txt new file mode 100644 index 0000000000..2f3fd83a51 --- /dev/null +++ b/fastlane/metadata/android/hy/title.txt @@ -0,0 +1 @@ +Element - Անվտանգ Մեսինջեր diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt index 282914188b..b54e04e1c8 100644 --- a/fastlane/metadata/android/vi/full_description.txt +++ b/fastlane/metadata/android/vi/full_description.txt @@ -1,42 +1,42 @@ Element vừa là một ứng dụng nhắn tin bảo mật vừa là một ứng dụng cộng tác nhóm năng suất, lý tưởng cho các cuộc trò chuyện nhóm khi làm việc từ xa. Ứng dụng trò chuyện này sử dụng mã hóa đầu cuối để cung cấp tính năng hội thảo truyền hình, chia sẻ tệp và cuộc gọi thoại mạnh mẽ. - Các tính năng của Element bao gồm: -- Các công cụ giao tiếp trực tuyến tiên tiến +Các tính năng của Element bao gồm: +- Công cụ giao tiếp trực tuyến nâng cao - Các tin nhắn được mã hóa hoàn toàn để cho phép liên lạc doanh nghiệp an toàn hơn, ngay cả đối với những người làm việc từ xa - Trò chuyện phi tập trung dựa trên khung mã nguồn mở Matrix -- Chia sẻ tệp một cách an toàn với dữ liệu được mã hóa trong khi quản lý dự án -- Trò chuyện video với gọi thoại qua giao thức Internet (IP - VoIP) và chia sẻ màn hình +- Chia sẻ tệp bảo mật với dữ liệu được mã hóa trong khi quản lý dự án +- Trò chuyện video với gọi thoại qua giao thức Internet (VoIP) và chia sẻ màn hình - Tích hợp dễ dàng với các công cụ cộng tác trực tuyến yêu thích của bạn, công cụ quản lý dự án, dịch vụ VoIP và các ứng dụng nhắn tin nhóm khác -Element hoàn toàn khác với các ứng dụng nhắn tin và cộng tác khác. Hoạt động trên Matrix, một mạng mở để nhắn tin bảo mật và giao tiếp phi tập trung. Đồng thời, cho phép tự lưu trữ để cung cấp cho người dùng quyền sở hữu và kiểm soát tối đa dữ liệu và tin nhắn của họ. +Element hoàn toàn khác với các ứng dụng nhắn tin và cộng tác khác. Hoạt động trên Matrix, một mạng mở để nhắn tin bảo mật và giao tiếp phi tập trung. Đồng thời, cho phép tự vận hành để cung cấp cho người dùng quyền sở hữu và kiểm soát tối đa dữ liệu và tin nhắn của họ. - Nhắn tin mã hóa và riêng tư -Element bảo vệ bạn khỏi các quảng cáo không mong muốn, khai thác dữ liệu và kiểm soát khu vực. Element cũng bảo mật tất cả dữ liệu của bạn, video 1-1 và giao tiếp thoại thông qua mã hóa đầu cuối và xác minh thiết bị có chữ ký chéo. +Nhắn tin mã hóa và riêng tư +Element bảo vệ bạn khỏi các quảng cáo không mong muốn, khai thác dữ liệu và kiểm soát khu vực. Element cũng bảo mật tất cả dữ liệu của bạn, truyền hình 1-1 và giao tiếp thoại thông qua mã hóa đầu cuối và xác thực chéo thiết bị. -Element cung cấp cho bạn quyền kiểm soát quyền riêng tư của mình đồng thời cho phép bạn giao tiếp an toàn với bất kỳ ai trên mạng Matrix hoặc các công cụ cộng tác kinh doanh khác bằng cách tích hợp với các ứng dụng như Slack. +Element cung cấp cho bạn quyền kiểm soát quyền riêng tư của mình đồng thời cho phép bạn giao tiếp bảo mật với bất kỳ ai trên mạng Matrix hoặc các công cụ cộng tác kinh doanh khác bằng cách tích hợp với các ứng dụng như Slack. - Element có thể được tự lưu trữ -Để cho phép kiểm soát nhiều hơn dữ liệu nhạy cảm và các cuộc trò chuyện của bạn, Element có thể được tự lưu trữ hoặc bạn có thể chọn bất kỳ máy chủ Matrix nào - tiêu chuẩn cho giao tiếp phi tập trung, mã nguồn mở. Element cung cấp cho bạn quyền riêng tư, tuân thủ bảo mật và tính linh hoạt trong tích hợp. +Element có thể được tự vận hành +Để cho phép kiểm soát nhiều hơn dữ liệu nhạy cảm và các cuộc trò chuyện của bạn, Element có thể được tự vận hành hoặc bạn có thể chọn bất kỳ máy chủ Matrix nào - tiêu chuẩn cho giao tiếp phi tập trung, mã nguồn mở. Element cung cấp cho bạn quyền riêng tư, tuân thủ bảo mật và tính linh hoạt trong tích hợp. - Sở hữu dữ liệu của bạn -Bạn quyết định nơi lưu giữ dữ liệu và tin nhắn của mình. Không có rủi ro khai thác dữ liệu hoặc truy cập từ bên thứ ba. +Sở hữu dữ liệu của bạn +Bạn quyết định nơi lưu trữ dữ liệu và tin nhắn của mình. Không có rủi ro khai thác dữ liệu hoặc truy cập từ bên thứ ba. Element giúp bạn kiểm soát theo những cách khác nhau: -1. Tạo một tài khoản miễn phí trên máy chủ công cộng matrix.org do các nhà phát triển Matrix vận hành hoặc chọn từ hàng nghìn máy chủ công cộng do các tình nguyện viên lưu trữ +1. Tạo một tài khoản miễn phí trên máy chủ công cộng matrix.org do các nhà phát triển Matrix vận hành hoặc chọn từ hàng nghìn máy chủ công cộng do các tình nguyện viên vận hành 2. Tự lưu trữ tài khoản của bạn bằng cách chạy một máy chủ trên cơ sở hạ tầng CNTT của riêng bạn 3. Đăng ký tài khoản trên máy chủ tùy chỉnh bằng cách chỉ cần đăng ký nền tảng Element Matrix Services hosting - Nhắn tin và cộng tác mở +Nhắn tin và cộng tác mở Bạn có thể trò chuyện với bất kỳ ai trên mạng Matrix, cho dù họ đang sử dụng Element, một ứng dụng Matrix khác hay ngay cả khi họ đang sử dụng một ứng dụng nhắn tin khác. - Siêu bảo mật -Mã hóa đầu-cuối thực (chỉ những người trong cuộc trò chuyện mới có thể giải mã tin nhắn) và xác minh thiết bị xác thực chéo. +Siêu bảo mật +Mã hóa đầu-cuối có thực (chỉ những người trong cuộc trò chuyện mới có thể giải mã tin nhắn) và xác thực chéo thiết bị. - Giao tiếp và tích hợp hoàn chỉnh -Nhắn tin, cuộc gọi thoại và video, chia sẻ tệp, chia sẻ màn hình và một loạt các tích hợp, bot và widget. Xây dựng phòng, cộng đồng, giữ liên lạc và hoàn thành công việc. +Giao tiếp và tích hợp hoàn chỉnh +Nhắn tin, gọi thoại và truyền hình, chia sẻ tệp, chia sẻ màn hình và một loạt các tích hợp, bot và widget. Tạo phòng, cộng đồng, giữ liên lạc và hoàn thành công việc. - Tiếp tục nơi bạn đã dừng lại +Tiếp tục nơi bạn đã dừng lạ Giữ liên lạc mọi lúc mọi nơi với lịch sử tin nhắn được đồng bộ hóa hoàn toàn trên tất cả các thiết bị của bạn và trên web tại https://app.element.io - Mã nguồn mở -Element Android là một dự án mã nguồn mở, được lưu trữ trên GitHub. Vui lòng báo cáo lỗi và / hoặc đóng góp phát triển tại https://github.com/vector-im/element-android +Mã nguồn mở +Element Android là một dự án mã nguồn mở, được lưu trữ trên GitHub. Vui lòng báo cáo lỗi và / hoặc đóng góp, phát triển tại https://github.com/vector-im/element-android diff --git a/library/external/autocomplete/build.gradle b/library/external/autocomplete/build.gradle new file mode 100644 index 0000000000..39d4d4b19e --- /dev/null +++ b/library/external/autocomplete/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "com.otaliastudios.autocomplete" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation libs.androidx.recyclerview +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java new file mode 100644 index 0000000000..cf58146f14 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java @@ -0,0 +1,434 @@ +package com.otaliastudios.autocomplete; + +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; + + +/** + * Entry point for adding Autocomplete behavior to a {@link EditText}. + * + * You can construct a {@code Autocomplete} using the builder provided by {@link Autocomplete#on(EditText)}. + * Building is enough, but you can hold a reference to this class to call its public methods. + * + * Requires: + * - {@link EditText}: this is both the anchor for the popup, and the source of text events that we listen to + * - {@link AutocompletePresenter}: this presents items in the popup window. See class for more info. + * - {@link AutocompleteCallback}: if specified, this listens to click events and visibility changes + * - {@link AutocompletePolicy}: if specified, this controls how and when to show the popup based on text events + * If not, this defaults to {@link SimplePolicy}: shows the popup when text.length() bigger than 0. + */ +public final class Autocomplete implements TextWatcher, SpanWatcher { + + private final static String TAG = Autocomplete.class.getSimpleName(); + private final static boolean DEBUG = false; + + private static void log(String log) { + if (DEBUG) Log.e(TAG, log); + } + + /** + * Builder for building {@link Autocomplete}. + * The only mandatory item is a presenter, {@link #with(AutocompletePresenter)}. + * + * @param the data model + */ + public final static class Builder { + private EditText source; + private AutocompletePresenter presenter; + private AutocompletePolicy policy; + private AutocompleteCallback callback; + private Drawable backgroundDrawable; + private float elevationDp = 6; + + private Builder(EditText source) { + this.source = source; + } + + /** + * Registers the {@link AutocompletePresenter} to be used, responsible for showing + * items. See the class for info. + * + * @param presenter desired presenter + * @return this for chaining + */ + public Builder with(AutocompletePresenter presenter) { + this.presenter = presenter; + return this; + } + + /** + * Registers the {@link AutocompleteCallback} to be used, responsible for listening to + * clicks provided by the presenter, and visibility changes. + * + * @param callback desired callback + * @return this for chaining + */ + public Builder with(AutocompleteCallback callback) { + this.callback = callback; + return this; + } + + /** + * Registers the {@link AutocompletePolicy} to be used, responsible for showing / dismissing + * the popup when certain events happen (e.g. certain characters are typed). + * + * @param policy desired policy + * @return this for chaining + */ + public Builder with(AutocompletePolicy policy) { + this.policy = policy; + return this; + } + + /** + * Sets a background drawable for the popup. + * + * @param backgroundDrawable drawable + * @return this for chaining + */ + public Builder with(Drawable backgroundDrawable) { + this.backgroundDrawable = backgroundDrawable; + return this; + } + + /** + * Sets elevation for the popup. Defaults to 6 dp. + * + * @param elevationDp popup elevation, in DP + * @return this for chaning. + */ + public Builder with(float elevationDp) { + this.elevationDp = elevationDp; + return this; + } + + /** + * Builds an Autocomplete instance. This is enough for autocomplete to be set up, + * but you can hold a reference to the object and call its public methods. + * + * @return an Autocomplete instance, if you need it + * + * @throws RuntimeException if either EditText or the presenter are null + */ + public Autocomplete build() { + if (source == null) throw new RuntimeException("Autocomplete needs a source!"); + if (presenter == null) throw new RuntimeException("Autocomplete needs a presenter!"); + if (policy == null) policy = new SimplePolicy(); + return new Autocomplete(this); + } + + private void clear() { + source = null; + presenter = null; + callback = null; + policy = null; + backgroundDrawable = null; + elevationDp = 6; + } + } + + /** + * Entry point for building autocomplete on a certain {@link EditText}. + * @param anchor the anchor for the popup, and the source of text events + * @param your data model + * @return a Builder for set up + */ + public static Builder on(EditText anchor) { + return new Builder(anchor); + } + + private AutocompletePolicy policy; + private AutocompletePopup popup; + private AutocompletePresenter presenter; + private AutocompleteCallback callback; + private EditText source; + + private boolean block; + private boolean disabled; + private boolean openBefore; + private String lastQuery = "null"; + + private Autocomplete(Builder builder) { + policy = builder.policy; + presenter = builder.presenter; + callback = builder.callback; + source = builder.source; + + // Set up popup + popup = new AutocompletePopup(source.getContext()); + popup.setAnchorView(source); + popup.setGravity(Gravity.START); + popup.setModal(false); + popup.setBackgroundDrawable(builder.backgroundDrawable); + popup.setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, builder.elevationDp, + source.getContext().getResources().getDisplayMetrics())); + + // popup dimensions + AutocompletePresenter.PopupDimensions dim = this.presenter.getPopupDimensions(); + popup.setWidth(dim.width); + popup.setHeight(dim.height); + popup.setMaxWidth(dim.maxWidth); + popup.setMaxHeight(dim.maxHeight); + + // Fire visibility events + popup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + lastQuery = "null"; + if (callback != null) callback.onPopupVisibilityChanged(false); + boolean saved = block; + block = true; + policy.onDismiss(source.getText()); + block = saved; + presenter.hideView(); + } + }); + + // Set up source + source.getText().setSpan(this, 0, source.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + source.addTextChangedListener(this); + + // Set up presenter + presenter.registerClickProvider(new AutocompletePresenter.ClickProvider() { + @Override + public void click(@NonNull T item) { + AutocompleteCallback callback = Autocomplete.this.callback; + EditText edit = Autocomplete.this.source; + if (callback == null) return; + boolean saved = block; + block = true; + boolean dismiss = callback.onPopupItemClicked(edit.getText(), item); + if (dismiss) dismissPopup(); + block = saved; + } + }); + + builder.clear(); + } + + /** + * Controls how the popup operates with an input method. + * + * If the popup is showing, calling this method will take effect only + * the next time the popup is shown. + * + * @param mode a {@link PopupWindow} input method mode + */ + public void setInputMethodMode(int mode) { + popup.setInputMethodMode(mode); + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see {@link WindowManager.LayoutParams#softInputMode} + */ + public void setSoftInputMode(int mode) { + popup.setSoftInputMode(mode); + } + + /** + * Shows the popup with the given query. + * There is rarely need to call this externally: it is already triggered by events on the anchor. + * To control when this is called, provide a good implementation of {@link AutocompletePolicy}. + * + * @param query query text. + */ + public void showPopup(@NonNull CharSequence query) { + if (isPopupShowing() && lastQuery.equals(query.toString())) return; + lastQuery = query.toString(); + + log("showPopup: called with filter "+query); + if (!isPopupShowing()) { + log("showPopup: showing"); + presenter.registerDataSetObserver(new Observer()); // Calling new to avoid leaking... maybe... + popup.setView(presenter.getView()); + presenter.showView(); + popup.show(); + if (callback != null) callback.onPopupVisibilityChanged(true); + } + log("showPopup: popup should be showing... "+isPopupShowing()); + presenter.onQuery(query); + } + + /** + * Dismisses the popup, if showing. + * There is rarely need to call this externally: it is already triggered by events on the anchor. + * To control when this is called, provide a good implementation of {@link AutocompletePolicy}. + */ + public void dismissPopup() { + if (isPopupShowing()) { + popup.dismiss(); + } + } + + /** + * Returns true if the popup is showing. + * @return whether the popup is currently showing + */ + public boolean isPopupShowing() { + return this.popup.isShowing(); + } + + /** + * Switch to control the autocomplete behavior. When disabled, no popup is shown. + * This is useful if you want to do runtime edits to the anchor text, without triggering + * the popup. + * + * @param enabled whether to enable autocompletion + */ + public void setEnabled(boolean enabled) { + disabled = !enabled; + } + + /** + * Sets the gravity for the popup. Basically only {@link Gravity#START} and {@link Gravity#END} + * do work. + * + * @param gravity gravity for the popup + */ + public void setGravity(int gravity) { + popup.setGravity(gravity); + } + + /** + * Controls the vertical offset of the popup from the EditText anchor. + * + * @param offset offset in pixels. + */ + public void setOffsetFromAnchor(int offset) { popup.setVerticalOffset(offset); } + + /** + * Controls whether the popup should listen to clicks outside its boundaries. + * + * @param outsideTouchable true to listen to outside clicks + */ + public void setOutsideTouchable(boolean outsideTouchable) { popup.setOutsideTouchable(outsideTouchable); } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (block || disabled) return; + openBefore = isPopupShowing(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (block || disabled) return; + if (openBefore && !isPopupShowing()) { + return; // Copied from somewhere. + } + if (!(s instanceof Spannable)) { + source.setText(new SpannableString(s)); + return; + } + Spannable sp = (Spannable) s; + + int cursor = source.getSelectionEnd(); + log("onTextChanged: cursor end position is "+cursor); + if (cursor == -1) { // No cursor present. + dismissPopup(); return; + } + if (cursor != source.getSelectionStart()) { + // Not sure about this. We should have no problems dealing with multi selections, + // we just take the end... + // dismissPopup(); return; + } + + boolean b = block; + block = true; // policy might add spans or other stuff. + if (isPopupShowing() && policy.shouldDismissPopup(sp, cursor)) { + log("onTextChanged: dismissing"); + dismissPopup(); + } else if (isPopupShowing() || policy.shouldShowPopup(sp, cursor)) { + // LOG.now("onTextChanged: updating with filter "+policy.getQuery(sp)); + showPopup(policy.getQuery(sp)); + } + block = b; + } + + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void onSpanAdded(Spannable text, Object what, int start, int end) {} + + @Override + public void onSpanRemoved(Spannable text, Object what, int start, int end) {} + + @Override + public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) { + if (disabled || block) return; + if (what == Selection.SELECTION_END) { + // Selection end changed from ostart to nstart. Trigger a check. + log("onSpanChanged: selection end moved from "+ostart+" to "+nstart); + log("onSpanChanged: block is "+block); + boolean b = block; + block = true; + if (!isPopupShowing() && policy.shouldShowPopup(text, nstart)) { + showPopup(policy.getQuery(text)); + } + block = b; + } + } + + private class Observer extends DataSetObserver implements Runnable { + private Handler ui = new Handler(Looper.getMainLooper()); + + @Override + public void onChanged() { + // ??? Not sure this is needed... + ui.post(this); + } + + @Override + public void run() { + if (isPopupShowing()) { + // Call show again to revisit width and height. + popup.show(); + } + } + } + + /** + * A very simple {@link AutocompletePolicy} implementation. + * Popup is shown when text length is bigger than 0, and hidden when text is empty. + * The query string is the whole text. + */ + public static class SimplePolicy implements AutocompletePolicy { + @Override + public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) { + return text.length() > 0; + } + + @Override + public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) { + return text.length() == 0; + } + + @NonNull + @Override + public CharSequence getQuery(@NonNull Spannable text) { + return text; + } + + @Override + public void onDismiss(@NonNull Spannable text) {} + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java new file mode 100644 index 0000000000..56870db610 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java @@ -0,0 +1,29 @@ +package com.otaliastudios.autocomplete; + +import android.text.Editable; + +import androidx.annotation.NonNull; + +/** + * Optional callback to be passed to {@link Autocomplete.Builder}. + */ +public interface AutocompleteCallback { + + /** + * Called when an item inside your list is clicked. + * This works if your presenter has dispatched a click event. + * At this point you can edit the text, e.g. {@code editable.append(item.toString())}. + * + * @param editable editable text that you can work on + * @param item item that was clicked + * @return true if the action is valid and the popup can be dismissed + */ + boolean onPopupItemClicked(@NonNull Editable editable, @NonNull T item); + + /** + * Called when popup visibility state changes. + * + * @param shown true if the popup was just shown, false if it was just hidden + */ + void onPopupVisibilityChanged(boolean shown); +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java new file mode 100644 index 0000000000..05dfaa9f0a --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java @@ -0,0 +1,64 @@ +package com.otaliastudios.autocomplete; + +import android.text.Spannable; + +import androidx.annotation.NonNull; + +/** + * This interface controls when to show or hide the popup window, and, in the first case, + * what text should be passed to the popup {@link AutocompletePresenter}. + * + * @see Autocomplete.SimplePolicy for the simplest possible implementation + */ +public interface AutocompletePolicy { + + /** + * Called to understand whether the popup should be shown. Some naive examples: + * - Show when there's text: {@code return text.length() > 0} + * - Show when last char is @: {@code return text.getCharAt(text.length()-1) == '@'} + * + * @param text current text, along with its Spans + * @param cursorPos the position of the cursor + * @return true if popup should be shown + */ + boolean shouldShowPopup(@NonNull Spannable text, int cursorPos); + + /** + * Called to understand whether a currently shown popup should be closed, maybe + * because text is invalid. A reasonable implementation is + * {@code return !shouldShowPopup(text, cursorPos)}. + * + * However this is defined so you can add or clear spans. + * + * @param text current text, along with its Spans + * @param cursorPos the position of the cursor + * @return true if popup should be hidden + */ + boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos); + + /** + * Called to understand which query should be passed to {@link AutocompletePresenter} + * for a showing popup. If this is called, {@link #shouldShowPopup(Spannable, int)} just returned + * true, or {@link #shouldDismissPopup(Spannable, int)} just returned false. + * + * This is useful to understand which part of the text should be passed to presenters. + * For example, user might have typed '@john' to select a username, but you just want to + * search for 'john'. + * + * For more complex cases, you can add inclusive Spans in {@link #shouldShowPopup(Spannable, int)}, + * and get the span position here. + * + * @param text current text, along with its Spans + * @return the query for presenter + */ + @NonNull + CharSequence getQuery(@NonNull Spannable text); + + /** + * Called when popup is dismissed. This can be used, for instance, to clear custom Spans + * from the text. + * + * @param text text at the moment of dismissing + */ + void onDismiss(@NonNull Spannable text); +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java new file mode 100644 index 0000000000..6467085068 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java @@ -0,0 +1,521 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.core.view.ViewCompat; +import androidx.core.widget.PopupWindowCompat; + +/** + * A simplified version of andriod.widget.ListPopupWindow, which is the class used by + * AutocompleteTextView. + * + * Other than being simplified, this deals with Views rather than ListViews, so the content + * can be whatever. Lots of logic (clicks, selections etc.) has been removed because we manage that + * in {@link AutocompletePresenter}. + * + */ +class AutocompletePopup { + private Context mContext; + private ViewGroup mView; + private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mMaxHeight = Integer.MAX_VALUE; + private int mMaxWidth = Integer.MAX_VALUE; + private int mUserMaxHeight = Integer.MAX_VALUE; + private int mUserMaxWidth = Integer.MAX_VALUE; + private int mHorizontalOffset = 0; + private int mVerticalOffset = 0; + private boolean mVerticalOffsetSet; + private int mGravity = Gravity.NO_GRAVITY; + private boolean mAlwaysVisible = false; + private boolean mOutsideTouchable = true; + private View mAnchorView; + private final Rect mTempRect = new Rect(); + private boolean mModal; + private PopupWindow mPopup; + + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + AutocompletePopup(@NonNull Context context) { + super(); + mContext = context; + mPopup = new PopupWindow(context); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + } + + + /** + * Set whether this window should be modal when shown. + * + *

If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + @SuppressWarnings("SameParameterValue") + void setModal(boolean modal) { + mModal = modal; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + @SuppressWarnings("unused") + boolean isModal() { + return mModal; + } + + void setElevation(float elevationPx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mPopup.setElevation(elevationPx); + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + */ + @SuppressWarnings("unused") + void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + */ + @SuppressWarnings("unused") + boolean isDropDownAlwaysVisible() { + return mAlwaysVisible; + } + + void setOutsideTouchable(boolean outsideTouchable) { + mOutsideTouchable = outsideTouchable; + } + + @SuppressWarnings("WeakerAccess") + boolean isOutsideTouchable() { + return mOutsideTouchable && !mAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + void setSoftInputMode(int mode) { + mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + int getSoftInputMode() { + return mPopup.getSoftInputMode(); + } + + /** + * @return The background drawable for the popup window. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + @Nullable + Drawable getBackground() { + return mPopup.getBackground(); + } + + /** + * Sets a drawable to be the background for the popup window. + * @param d A drawable to set as the background. + */ + void setBackgroundDrawable(@Nullable Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * @param animationStyle Animation style to use. + */ + @SuppressWarnings("unused") + void setAnimationStyle(@StyleRes int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is + * shown or dismissed. + * @return Animation style that will be used. + */ + @SuppressWarnings("unused") + @StyleRes + int getAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * @return The popup's anchor view + */ + @SuppressWarnings("WeakerAccess") + View getAnchorView() { + return mAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to + * the anchor view when shown. + * @param anchor The view to use as an anchor. + */ + void setAnchorView(@NonNull View anchor) { + mAnchorView = anchor; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * @param offset The horizontal offset of the popup from its anchor. + */ + @SuppressWarnings("unused") + void setHorizontalOffset(int offset) { + mHorizontalOffset = offset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * @param offset The vertical offset of the popup from its anchor. + */ + void setVerticalOffset(int offset) { + mVerticalOffset = offset; + mVerticalOffsetSet = true; + } + + /** + * Set the gravity of the dropdown list. This is commonly used to + * set gravity to START or END for alignment with the anchor. + * @param gravity Gravity value to use + */ + void setGravity(int gravity) { + mGravity = gravity; + } + + /** + * @return The width of the popup window in pixels. + */ + @SuppressWarnings("unused") + int getWidth() { + return mWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be MATCH_PARENT + * or WRAP_CONTENT. + * @param width Width of the popup window. + */ + void setWidth(int width) { + mWidth = width; + } + + /** + * Sets the width of the popup window by the size of its content. The final width may be + * larger to accommodate styled window dressing. + * @param width Desired width of content in pixels. + */ + @SuppressWarnings("unused") + void setContentWidth(int width) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + width += mTempRect.left + mTempRect.right; + } + setWidth(width); + } + + void setMaxWidth(int width) { + if (width > 0) { + mUserMaxWidth = width; + } + } + + /** + * @return The height of the popup window in pixels. + */ + @SuppressWarnings("unused") + int getHeight() { + return mHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be MATCH_PARENT. + * @param height Height of the popup window. + */ + void setHeight(int height) { + mHeight = height; + } + + /** + * Sets the height of the popup window by the size of its content. The final height may be + * larger to accommodate styled window dressing. + * @param height Desired height of content in pixels. + */ + @SuppressWarnings("unused") + void setContentHeight(int height) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + height += mTempRect.top + mTempRect.bottom; + } + setHeight(height); + } + + void setMaxHeight(int height) { + if (height > 0) { + mUserMaxHeight = height; + } + } + + void setOnDismissListener(PopupWindow.OnDismissListener listener) { + mPopup.setOnDismissListener(listener); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + void show() { + if (!ViewCompat.isAttachedToWindow(getAnchorView())) return; + + int height = buildDropDown(); + final boolean noInputMethod = isInputMethodNotNeeded(); + int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; + PopupWindowCompat.setWindowLayoutType(mPopup, mDropDownWindowLayoutType); + + if (mPopup.isShowing()) { + // First pass for this special case, don't know why. + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + int tempWidth = mWidth == ViewGroup.LayoutParams.MATCH_PARENT ? ViewGroup.LayoutParams.MATCH_PARENT : 0; + if (noInputMethod) { + mPopup.setWidth(tempWidth); + mPopup.setHeight(0); + } else { + mPopup.setWidth(tempWidth); + mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); + } + } + + // The call to PopupWindow's update method below can accept -1 + // for any value you do not want to update. + + // Width. + int widthSpec; + if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = -1; + } else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mWidth; + } + widthSpec = Math.min(widthSpec, mMaxWidth); + widthSpec = (widthSpec < 0) ? - 1 : widthSpec; + + // Height. + int heightSpec; + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mHeight; + } + heightSpec = Math.min(heightSpec, mMaxHeight); + heightSpec = (heightSpec < 0) ? - 1 : heightSpec; + + // Update. + mPopup.setOutsideTouchable(isOutsideTouchable()); + if (heightSpec == 0) { + dismiss(); + } else { + mPopup.update(getAnchorView(), mHorizontalOffset, mVerticalOffset, widthSpec, heightSpec); + } + + } else { + int widthSpec; + if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mWidth; + } + widthSpec = Math.min(widthSpec, mMaxWidth); + + int heightSpec; + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mHeight; + } + heightSpec = Math.min(heightSpec, mMaxHeight); + + // Set width and height. + mPopup.setWidth(widthSpec); + mPopup.setHeight(heightSpec); + mPopup.setClippingEnabled(true); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(isOutsideTouchable()); + PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mHorizontalOffset, mVerticalOffset, mGravity); + } + } + + /** + * Dismiss the popup window. + */ + void dismiss() { + mPopup.dismiss(); + mPopup.setContentView(null); + mView = null; + } + + /** + * Control how the popup operates with an input method: one of + * INPUT_METHOD_FROM_FOCUSABLE, INPUT_METHOD_NEEDED, + * or INPUT_METHOD_NOT_NEEDED. + * + *

If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.

+ * + * @see #show() + */ + void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + @SuppressWarnings("WeakerAccess") + boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + } + + + void setView(ViewGroup view) { + mView = view; + mView.setFocusable(true); + mView.setFocusableInTouchMode(true); + ViewGroup dropDownView = mView; + mPopup.setContentView(dropDownView); + ViewGroup.LayoutParams params = mView.getLayoutParams(); + if (params != null) { + if (params.height > 0) setHeight(params.height); + if (params.width > 0) setWidth(params.width); + } + } + + /** + *

Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.

+ * + * @return the content's wrap content height or -1 if content already exists + */ + private int buildDropDown() { + int otherHeights = 0; + + // getMaxAvailableHeight() subtracts the padding, so we put it back + // to get the available height for the whole window. + final int paddingVert; + final int paddingHoriz; + final Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + paddingVert = mTempRect.top + mTempRect.bottom; + paddingHoriz = mTempRect.left + mTempRect.right; + + // If we don't have an explicit vertical offset, determine one from + // the window background so that content will line up. + if (!mVerticalOffsetSet) { + mVerticalOffset = -mTempRect.top; + } + } else { + mTempRect.setEmpty(); + paddingVert = 0; + paddingHoriz = 0; + } + + // Redefine dimensions taking into account maxWidth and maxHeight. + final boolean ignoreBottomDecorations = mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + final int maxContentHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? + mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset, ignoreBottomDecorations) : + mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset); + final int maxContentWidth = mContext.getResources().getDisplayMetrics().widthPixels - paddingHoriz; + + mMaxHeight = Math.min(maxContentHeight + paddingVert, mUserMaxHeight); + mMaxWidth = Math.min(maxContentWidth + paddingHoriz, mUserMaxWidth); + // if (mHeight > 0) mHeight = Math.min(mHeight, maxContentHeight); + // if (mWidth > 0) mWidth = Math.min(mWidth, maxContentWidth); + + if (mAlwaysVisible || mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return mMaxHeight; + } + + final int childWidthSpec; + switch (mWidth) { + case ViewGroup.LayoutParams.WRAP_CONTENT: + childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST); break; + case ViewGroup.LayoutParams.MATCH_PARENT: + childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.EXACTLY); break; + default: + //noinspection Range + childWidthSpec = View.MeasureSpec.makeMeasureSpec(mWidth, View.MeasureSpec.EXACTLY); break; + } + + // Add padding only if the list has items in it, that way we don't show + // the popup if it is not needed. For this reason, we measure as wrap_content. + mView.measure(childWidthSpec, View.MeasureSpec.makeMeasureSpec(maxContentHeight, View.MeasureSpec.AT_MOST)); + final int viewHeight = mView.getMeasuredHeight(); + if (viewHeight > 0) { + otherHeights += paddingVert + mView.getPaddingTop() + mView.getPaddingBottom(); + } + + return Math.min(viewHeight + otherHeights, mMaxHeight); + } + + +} \ No newline at end of file diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java new file mode 100644 index 0000000000..d49e8c8f30 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java @@ -0,0 +1,129 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Base class for presenting items inside a popup. This is abstract and must be implemented. + * + * Most important methods are {@link #getView()} and {@link #onQuery(CharSequence)}. + */ +public abstract class AutocompletePresenter { + + private Context context; + private boolean isShowing; + + @SuppressWarnings("WeakerAccess") + public AutocompletePresenter(@NonNull Context context) { + this.context = context; + } + + /** + * At this point the presenter is passed the {@link ClickProvider}. + * The contract is that {@link ClickProvider#click(Object)} must be called when a list item + * is clicked. This ensure that the autocomplete callback will receive the event. + * + * @param provider a click provider for this presenter. + */ + protected void registerClickProvider(ClickProvider provider) { + + } + + /** + * Useful if you wish to change width/height based on content height. + * The contract is to call {@link DataSetObserver#onChanged()} when your view has + * changes. + * + * This is called after {@link #getView()}. + * + * @param observer the observer. + */ + protected void registerDataSetObserver(@NonNull DataSetObserver observer) {} + + /** + * Called each time the popup is shown. You are meant to inflate the view here. + * You can get a LayoutInflater using {@link #getContext()}. + * + * @return a ViewGroup for the popup + */ + @NonNull + protected abstract ViewGroup getView(); + + /** + * Provide the {@link PopupDimensions} for this popup. Called just once. + * You can use fixed dimensions or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}. + * + * @return a PopupDimensions object + */ + // Called at first to understand which dimensions to use for the popup. + @NonNull + protected PopupDimensions getPopupDimensions() { + return new PopupDimensions(); + } + + /** + * Perform firther initialization here. Called after {@link #getView()}, + * each time the popup is shown. + */ + protected abstract void onViewShown(); + + /** + * Called to update the view to filter results with the query. + * It is called any time the popup is shown, and any time the text changes and query is updated. + * + * @param query query from the edit text, to filter our results + */ + protected abstract void onQuery(@Nullable CharSequence query); + + /** + * Called when the popup is hidden, to release resources. + */ + protected abstract void onViewHidden(); + + /** + * @return this presenter context + */ + @NonNull + protected final Context getContext() { + return context; + } + + /** + * @return whether we are showing currently + */ + @SuppressWarnings("unused") + protected final boolean isShowing() { + return isShowing; + } + + final void showView() { + isShowing = true; + onViewShown(); + } + + final void hideView() { + isShowing = false; + onViewHidden(); + } + + public interface ClickProvider { + void click(@NonNull T item); + } + + /** + * Provides width, height, maxWidth and maxHeight for the popup. + * @see #getPopupDimensions() + */ + @SuppressWarnings("WeakerAccess") + public static class PopupDimensions { + public int width = ViewGroup.LayoutParams.WRAP_CONTENT; + public int height = ViewGroup.LayoutParams.WRAP_CONTENT; + public int maxWidth = Integer.MAX_VALUE; + public int maxHeight = Integer.MAX_VALUE; + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java new file mode 100644 index 0000000000..9c94983317 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java @@ -0,0 +1,184 @@ +package com.otaliastudios.autocomplete; + +import android.text.Spannable; +import android.text.Spanned; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + + +/** + * A special {@link AutocompletePolicy} for cases when you want to trigger the popup when a + * certain character is shown. + * + * For instance, this might be the case for hashtags ('#') or usernames ('@') or whatever you wish. + * Passing this to {@link Autocomplete.Builder} ensures the following behavior (assuming '@'): + * - text "@john" : presenter will be passed the query "john" + * - text "You should see this @j" : presenter will be passed the query "j" + * - text "You should see this @john @m" : presenter will be passed the query "m" + */ +public class CharPolicy implements AutocompletePolicy { + + private final static String TAG = CharPolicy.class.getSimpleName(); + private final static boolean DEBUG = false; + + private static void log(@NonNull String log) { + if (DEBUG) Log.e(TAG, log); + } + + private final char CH; + private final int[] INT = new int[2]; + private boolean needSpaceBefore = true; + + /** + * Constructs a char policy for the given character. + * + * @param trigger the triggering character. + */ + public CharPolicy(char trigger) { + CH = trigger; + } + + /** + * Constructs a char policy for the given character. + * You can choose whether a whitespace is needed before 'trigger'. + * + * @param trigger the triggering character. + * @param needSpaceBefore whether we need a space before trigger + */ + @SuppressWarnings("unused") + public CharPolicy(char trigger, boolean needSpaceBefore) { + CH = trigger; + this.needSpaceBefore = needSpaceBefore; + } + + /** + * Can be overriden to understand which characters are valid. The default implementation + * returns true for any character except whitespaces. + * + * @param ch the character + * @return whether it's valid part of a query + */ + @SuppressWarnings("WeakerAccess") + protected boolean isValidChar(char ch) { + return !Character.isWhitespace(ch); + } + + @Nullable + private int[] checkText(@NonNull Spannable text, int cursorPos) { + final int spanEnd = cursorPos; + char last = 'x'; + cursorPos -= 1; // If the cursor is at the end, we will have cursorPos = length. Go back by 1. + while (cursorPos >= 0 && last != CH) { + char ch = text.charAt(cursorPos); + log("checkText: char is "+ch); + if (isValidChar(ch)) { + // We are going back + log("checkText: char is valid"); + cursorPos -= 1; + last = ch; + } else { + // We got a whitespace before getting a CH. This is invalid. + log("checkText: char is not valid, returning NULL"); + return null; + } + } + cursorPos += 1; // + 1 because we end BEHIND the valid selection + + // Start checking. + if (cursorPos == 0 && last != CH) { + // We got to the start of the string, and no CH was encountered. Nothing to do. + log("checkText: got to start but no CH, returning NULL"); + return null; + } + + // Additional checks for cursorPos - 1 + if (cursorPos > 0 && needSpaceBefore) { + char ch = text.charAt(cursorPos-1); + if (!Character.isWhitespace(ch)) { + log("checkText: char before is not whitespace, returning NULL"); + return null; + } + } + + // All seems OK. + final int spanStart = cursorPos + 1; // + 1 because we want to exclude CH from the query + INT[0] = spanStart; + INT[1] = spanEnd; + log("checkText: found! cursorPos="+cursorPos); + log("checkText: found! spanStart="+spanStart); + log("checkText: found! spanEnd="+spanEnd); + return INT; + } + + @Override + public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) { + // Returning true if, right before cursorPos, we have a word starting with @. + log("shouldShowPopup: text is "+text); + log("shouldShowPopup: cursorPos is "+cursorPos); + int[] show = checkText(text, cursorPos); + if (show != null) { + text.setSpan(new QuerySpan(), show[0], show[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE); + return true; + } + log("shouldShowPopup: returning false"); + return false; + } + + @Override + public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) { + log("shouldDismissPopup: text is "+text); + log("shouldDismissPopup: cursorPos is "+cursorPos); + boolean dismiss = checkText(text, cursorPos) == null; + log("shouldDismissPopup: returning "+dismiss); + return dismiss; + } + + @NonNull + @Override + public CharSequence getQuery(@NonNull Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) { + // Should never happen. + log("getQuery: there's no span!"); + return ""; + } + log("getQuery: found spans: "+span.length); + QuerySpan sp = span[0]; + log("getQuery: span start is "+text.getSpanStart(sp)); + log("getQuery: span end is "+text.getSpanEnd(sp)); + CharSequence seq = text.subSequence(text.getSpanStart(sp), text.getSpanEnd(sp)); + log("getQuery: returning "+seq); + return seq; + } + + + @Override + public void onDismiss(@NonNull Spannable text) { + // Remove any span added by shouldShow. Should be useless, but anyway. + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + for (QuerySpan s : span) { + text.removeSpan(s); + } + } + + private static class QuerySpan {} + + /** + * Returns the current query out of the given Spannable. + * @param text the anchor text + * @return an int[] with query start and query end positions + */ + @Nullable + public static int[] getQueryRange(@NonNull Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) return null; + if (span.length > 1) { + // Won't happen + log("getQueryRange: ERR: MORE THAN ONE QuerySpan."); + } + QuerySpan sp = span[0]; + return new int[]{text.getSpanStart(sp), text.getSpanEnd(sp)}; + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java new file mode 100644 index 0000000000..1338ec7cac --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java @@ -0,0 +1,152 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.ViewGroup; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Simple {@link AutocompletePresenter} implementation that hosts a {@link RecyclerView}. + * Supports {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} natively. + * The only contract is to + * + * - provide a {@link RecyclerView.Adapter} in {@link #instantiateAdapter()} + * - call {@link #dispatchClick(Object)} when an object is clicked + * - update your data during {@link #onQuery(CharSequence)} + * + * @param your model object (the object displayed by the list) + */ +public abstract class RecyclerViewPresenter extends AutocompletePresenter { + + private RecyclerView recycler; + private ClickProvider clicks; + private Observer observer; + + public RecyclerViewPresenter(@NonNull Context context) { + super(context); + } + + @Override + protected final void registerClickProvider(@NonNull ClickProvider provider) { + this.clicks = provider; + } + + @Override + protected final void registerDataSetObserver(@NonNull DataSetObserver observer) { + this.observer = new Observer(observer); + } + + @NonNull + @Override + protected ViewGroup getView() { + recycler = new RecyclerView(getContext()); + RecyclerView.Adapter adapter = instantiateAdapter(); + recycler.setAdapter(adapter); + recycler.setLayoutManager(instantiateLayoutManager()); + if (observer != null) { + adapter.registerAdapterDataObserver(observer); + observer = null; + } + return recycler; + } + + @Override + protected void onViewShown() {} + + @CallSuper + @Override + protected void onViewHidden() { + recycler = null; + observer = null; + } + + @SuppressWarnings("unused") + @Nullable + protected final RecyclerView getRecyclerView() { + return recycler; + } + + /** + * Dispatch click event to {@link AutocompleteCallback}. + * Should be called when items are clicked. + * + * @param item the clicked item. + */ + protected final void dispatchClick(@NonNull T item) { + if (clicks != null) clicks.click(item); + } + + /** + * Request that the popup should recompute its dimensions based on a recent change in + * the view being displayed. + * + * This is already managed internally for {@link RecyclerView} events. + * Only use it for changes in other views that you have added to the popup, + * and only if one of the dimensions for the popup is WRAP_CONTENT . + */ + @SuppressWarnings("unused") + protected final void dispatchLayoutChange() { + if (observer != null) observer.onChanged(); + } + + /** + * Provide an adapter for the recycler. + * This should be a fresh instance every time this is called. + * + * @return a new adapter. + */ + @NonNull + protected abstract RecyclerView.Adapter instantiateAdapter(); + + /** + * Provides a layout manager for the recycler. + * This should be a fresh instance every time this is called. + * Defaults to a vertical LinearLayoutManager, which is guaranteed to work well. + * + * @return a new layout manager. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + protected RecyclerView.LayoutManager instantiateLayoutManager() { + return new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); + } + + private final static class Observer extends RecyclerView.AdapterDataObserver { + + private DataSetObserver root; + + Observer(@NonNull DataSetObserver root) { + this.root = root; + } + + @Override + public void onChanged() { + root.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + root.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + root.onChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + root.onChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + root.onChanged(); + } + } +} diff --git a/library/external/barcodescanner/core/build.gradle b/library/external/barcodescanner/core/build.gradle new file mode 100644 index 0000000000..583b435dd7 --- /dev/null +++ b/library/external/barcodescanner/core/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + namespace "me.dm7.barcodescanner.core" + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +dependencies { + implementation 'androidx.annotation:annotation-jvm:1.6.0' +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java new file mode 100644 index 0000000000..ecfba76d89 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java @@ -0,0 +1,339 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.ColorInt; + +public abstract class BarcodeScannerView extends FrameLayout implements Camera.PreviewCallback { + + private CameraWrapper mCameraWrapper; + private CameraPreview mPreview; + private IViewFinder mViewFinderView; + private Rect mFramingRectInPreview; + private CameraHandlerThread mCameraHandlerThread; + private Boolean mFlashState; + private boolean mAutofocusState = true; + private boolean mShouldScaleToFill = true; + + private boolean mIsLaserEnabled = true; + @ColorInt private int mLaserColor = getResources().getColor(R.color.viewfinder_laser); + @ColorInt private int mBorderColor = getResources().getColor(R.color.viewfinder_border); + private int mMaskColor = getResources().getColor(R.color.viewfinder_mask); + private int mBorderWidth = getResources().getInteger(R.integer.viewfinder_border_width); + private int mBorderLength = getResources().getInteger(R.integer.viewfinder_border_length); + private boolean mRoundedCorner = false; + private int mCornerRadius = 0; + private boolean mSquaredFinder = false; + private float mBorderAlpha = 1.0f; + private int mViewFinderOffset = 0; + private float mAspectTolerance = 0.1f; + + public BarcodeScannerView(Context context) { + super(context); + init(); + } + + public BarcodeScannerView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + + TypedArray a = context.getTheme().obtainStyledAttributes( + attributeSet, + R.styleable.BarcodeScannerView, + 0, 0); + + try { + setShouldScaleToFill(a.getBoolean(R.styleable.BarcodeScannerView_shouldScaleToFill, true)); + mIsLaserEnabled = a.getBoolean(R.styleable.BarcodeScannerView_laserEnabled, mIsLaserEnabled); + mLaserColor = a.getColor(R.styleable.BarcodeScannerView_laserColor, mLaserColor); + mBorderColor = a.getColor(R.styleable.BarcodeScannerView_borderColor, mBorderColor); + mMaskColor = a.getColor(R.styleable.BarcodeScannerView_maskColor, mMaskColor); + mBorderWidth = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderWidth, mBorderWidth); + mBorderLength = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderLength, mBorderLength); + + mRoundedCorner = a.getBoolean(R.styleable.BarcodeScannerView_roundedCorner, mRoundedCorner); + mCornerRadius = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_cornerRadius, mCornerRadius); + mSquaredFinder = a.getBoolean(R.styleable.BarcodeScannerView_squaredFinder, mSquaredFinder); + mBorderAlpha = a.getFloat(R.styleable.BarcodeScannerView_borderAlpha, mBorderAlpha); + mViewFinderOffset = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_finderOffset, mViewFinderOffset); + } finally { + a.recycle(); + } + + init(); + } + + private void init() { + mViewFinderView = createViewFinderView(getContext()); + } + + public final void setupLayout(CameraWrapper cameraWrapper) { + removeAllViews(); + + mPreview = new CameraPreview(getContext(), cameraWrapper, this); + mPreview.setAspectTolerance(mAspectTolerance); + mPreview.setShouldScaleToFill(mShouldScaleToFill); + if (!mShouldScaleToFill) { + RelativeLayout relativeLayout = new RelativeLayout(getContext()); + relativeLayout.setGravity(Gravity.CENTER); + relativeLayout.setBackgroundColor(Color.BLACK); + relativeLayout.addView(mPreview); + addView(relativeLayout); + } else { + addView(mPreview); + } + + if (mViewFinderView instanceof View) { + addView((View) mViewFinderView); + } else { + throw new IllegalArgumentException("IViewFinder object returned by " + + "'createViewFinderView()' should be instance of android.view.View"); + } + } + + /** + *

Method that creates view that represents visual appearance of a barcode scanner

+ *

Override it to provide your own view for visual appearance of a barcode scanner

+ * + * @param context {@link Context} + * @return {@link android.view.View} that implements {@link ViewFinderView} + */ + protected IViewFinder createViewFinderView(Context context) { + ViewFinderView viewFinderView = new ViewFinderView(context); + viewFinderView.setBorderColor(mBorderColor); + viewFinderView.setLaserColor(mLaserColor); + viewFinderView.setLaserEnabled(mIsLaserEnabled); + viewFinderView.setBorderStrokeWidth(mBorderWidth); + viewFinderView.setBorderLineLength(mBorderLength); + viewFinderView.setMaskColor(mMaskColor); + + viewFinderView.setBorderCornerRounded(mRoundedCorner); + viewFinderView.setBorderCornerRadius(mCornerRadius); + viewFinderView.setSquareViewFinder(mSquaredFinder); + viewFinderView.setViewFinderOffset(mViewFinderOffset); + return viewFinderView; + } + + public void setLaserColor(int laserColor) { + mLaserColor = laserColor; + mViewFinderView.setLaserColor(mLaserColor); + mViewFinderView.setupViewFinder(); + } + public void setMaskColor(int maskColor) { + mMaskColor = maskColor; + mViewFinderView.setMaskColor(mMaskColor); + mViewFinderView.setupViewFinder(); + } + public void setBorderColor(int borderColor) { + mBorderColor = borderColor; + mViewFinderView.setBorderColor(mBorderColor); + mViewFinderView.setupViewFinder(); + } + public void setBorderStrokeWidth(int borderStrokeWidth) { + mBorderWidth = borderStrokeWidth; + mViewFinderView.setBorderStrokeWidth(mBorderWidth); + mViewFinderView.setupViewFinder(); + } + public void setBorderLineLength(int borderLineLength) { + mBorderLength = borderLineLength; + mViewFinderView.setBorderLineLength(mBorderLength); + mViewFinderView.setupViewFinder(); + } + public void setLaserEnabled(boolean isLaserEnabled) { + mIsLaserEnabled = isLaserEnabled; + mViewFinderView.setLaserEnabled(mIsLaserEnabled); + mViewFinderView.setupViewFinder(); + } + public void setIsBorderCornerRounded(boolean isBorderCornerRounded) { + mRoundedCorner = isBorderCornerRounded; + mViewFinderView.setBorderCornerRounded(mRoundedCorner); + mViewFinderView.setupViewFinder(); + } + public void setBorderCornerRadius(int borderCornerRadius) { + mCornerRadius = borderCornerRadius; + mViewFinderView.setBorderCornerRadius(mCornerRadius); + mViewFinderView.setupViewFinder(); + } + public void setSquareViewFinder(boolean isSquareViewFinder) { + mSquaredFinder = isSquareViewFinder; + mViewFinderView.setSquareViewFinder(mSquaredFinder); + mViewFinderView.setupViewFinder(); + } + public void setBorderAlpha(float borderAlpha) { + mBorderAlpha = borderAlpha; + mViewFinderView.setBorderAlpha(mBorderAlpha); + mViewFinderView.setupViewFinder(); + } + + public void startCamera(int cameraId) { + if(mCameraHandlerThread == null) { + mCameraHandlerThread = new CameraHandlerThread(this); + } + mCameraHandlerThread.startCamera(cameraId); + } + + public void setupCameraPreview(CameraWrapper cameraWrapper) { + mCameraWrapper = cameraWrapper; + if(mCameraWrapper != null) { + setupLayout(mCameraWrapper); + mViewFinderView.setupViewFinder(); + if(mFlashState != null) { + setFlash(mFlashState); + } + setAutoFocus(mAutofocusState); + } + } + + public void startCamera() { + startCamera(CameraUtils.getDefaultCameraId()); + } + + public void stopCamera() { + if(mCameraWrapper != null) { + mPreview.stopCameraPreview(); + mPreview.setCamera(null, null); + mCameraWrapper.mCamera.release(); + mCameraWrapper = null; + } + if(mCameraHandlerThread != null) { + mCameraHandlerThread.quit(); + mCameraHandlerThread = null; + } + } + + public void stopCameraPreview() { + if(mPreview != null) { + mPreview.stopCameraPreview(); + } + } + + protected void resumeCameraPreview() { + if(mPreview != null) { + mPreview.showCameraPreview(); + } + } + + public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) { + if (mFramingRectInPreview == null) { + Rect framingRect = mViewFinderView.getFramingRect(); + int viewFinderViewWidth = mViewFinderView.getWidth(); + int viewFinderViewHeight = mViewFinderView.getHeight(); + if (framingRect == null || viewFinderViewWidth == 0 || viewFinderViewHeight == 0) { + return null; + } + + Rect rect = new Rect(framingRect); + + if(previewWidth < viewFinderViewWidth) { + rect.left = rect.left * previewWidth / viewFinderViewWidth; + rect.right = rect.right * previewWidth / viewFinderViewWidth; + } + + if(previewHeight < viewFinderViewHeight) { + rect.top = rect.top * previewHeight / viewFinderViewHeight; + rect.bottom = rect.bottom * previewHeight / viewFinderViewHeight; + } + + mFramingRectInPreview = rect; + } + return mFramingRectInPreview; + } + + public void setFlash(boolean flag) { + mFlashState = flag; + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(flag) { + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + return; + } + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + } else { + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) { + return; + } + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + } + mCameraWrapper.mCamera.setParameters(parameters); + } + } + + public boolean getFlash() { + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + return true; + } else { + return false; + } + } + return false; + } + + public void toggleFlash() { + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + } else { + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + } + mCameraWrapper.mCamera.setParameters(parameters); + } + } + + public void setAutoFocus(boolean state) { + mAutofocusState = state; + if(mPreview != null) { + mPreview.setAutoFocus(state); + } + } + + public void setShouldScaleToFill(boolean shouldScaleToFill) { + mShouldScaleToFill = shouldScaleToFill; + } + + public void setAspectTolerance(float aspectTolerance) { + mAspectTolerance = aspectTolerance; + } + + public byte[] getRotatedData(byte[] data, Camera camera) { + Camera.Parameters parameters = camera.getParameters(); + Camera.Size size = parameters.getPreviewSize(); + int width = size.width; + int height = size.height; + + int rotationCount = getRotationCount(); + + if(rotationCount == 1 || rotationCount == 3) { + for (int i = 0; i < rotationCount; i++) { + byte[] rotatedData = new byte[data.length]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) + rotatedData[x * height + height - y - 1] = data[x + y * width]; + } + data = rotatedData; + int tmp = width; + width = height; + height = tmp; + } + } + + return data; + } + + public int getRotationCount() { + int displayOrientation = mPreview.getDisplayOrientation(); + return displayOrientation / 90; + } +} + diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java new file mode 100644 index 0000000000..2d4bcee7bf --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java @@ -0,0 +1,37 @@ +package me.dm7.barcodescanner.core; + + +import android.hardware.Camera; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +// This code is mostly based on the top answer here: http://stackoverflow.com/questions/18149964/best-use-of-handlerthread-over-other-similar-classes +public class CameraHandlerThread extends HandlerThread { + private static final String LOG_TAG = "CameraHandlerThread"; + + private BarcodeScannerView mScannerView; + + public CameraHandlerThread(BarcodeScannerView scannerView) { + super("CameraHandlerThread"); + mScannerView = scannerView; + start(); + } + + public void startCamera(final int cameraId) { + Handler localHandler = new Handler(getLooper()); + localHandler.post(new Runnable() { + @Override + public void run() { + final Camera camera = CameraUtils.getCameraInstance(cameraId); + Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post(new Runnable() { + @Override + public void run() { + mScannerView.setupCameraPreview(CameraWrapper.getWrapper(camera, cameraId)); + } + }); + } + }); + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java new file mode 100644 index 0000000000..b066e25b2c --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java @@ -0,0 +1,312 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.hardware.Camera; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import java.util.List; + +public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { + private static final String TAG = "CameraPreview"; + + private CameraWrapper mCameraWrapper; + private Handler mAutoFocusHandler; + private boolean mPreviewing = true; + private boolean mAutoFocus = true; + private boolean mSurfaceCreated = false; + private boolean mShouldScaleToFill = true; + private Camera.PreviewCallback mPreviewCallback; + private float mAspectTolerance = 0.1f; + + public CameraPreview(Context context, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + super(context); + init(cameraWrapper, previewCallback); + } + + public CameraPreview(Context context, AttributeSet attrs, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + super(context, attrs); + init(cameraWrapper, previewCallback); + } + + public void init(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + setCamera(cameraWrapper, previewCallback); + mAutoFocusHandler = new Handler(); + getHolder().addCallback(this); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + public void setCamera(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + mCameraWrapper = cameraWrapper; + mPreviewCallback = previewCallback; + } + + public void setShouldScaleToFill(boolean scaleToFill) { + mShouldScaleToFill = scaleToFill; + } + + public void setAspectTolerance(float aspectTolerance) { + mAspectTolerance = aspectTolerance; + } + + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + mSurfaceCreated = true; + } + + @Override + public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) { + if(surfaceHolder.getSurface() == null) { + return; + } + stopCameraPreview(); + showCameraPreview(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + mSurfaceCreated = false; + stopCameraPreview(); + } + + public void showCameraPreview() { + if(mCameraWrapper != null) { + try { + getHolder().addCallback(this); + mPreviewing = true; + setupCameraParameters(); + mCameraWrapper.mCamera.setPreviewDisplay(getHolder()); + mCameraWrapper.mCamera.setDisplayOrientation(getDisplayOrientation()); + mCameraWrapper.mCamera.setOneShotPreviewCallback(mPreviewCallback); + mCameraWrapper.mCamera.startPreview(); + if(mAutoFocus) { + if (mSurfaceCreated) { // check if surface created before using autofocus + safeAutoFocus(); + } else { + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } + } catch (Exception e) { + Log.e(TAG, e.toString(), e); + } + } + } + + public void safeAutoFocus() { + try { + mCameraWrapper.mCamera.autoFocus(autoFocusCB); + } catch (RuntimeException re) { + // Horrible hack to deal with autofocus errors on Sony devices + // See https://github.com/dm77/barcodescanner/issues/7 for example + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } + + public void stopCameraPreview() { + if(mCameraWrapper != null) { + try { + mPreviewing = false; + getHolder().removeCallback(this); + mCameraWrapper.mCamera.cancelAutoFocus(); + mCameraWrapper.mCamera.setOneShotPreviewCallback(null); + mCameraWrapper.mCamera.stopPreview(); + } catch(Exception e) { + Log.e(TAG, e.toString(), e); + } + } + } + + public void setupCameraParameters() { + Camera.Size optimalSize = getOptimalPreviewSize(); + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + parameters.setPreviewSize(optimalSize.width, optimalSize.height); + mCameraWrapper.mCamera.setParameters(parameters); + adjustViewSize(optimalSize); + } + + private void adjustViewSize(Camera.Size cameraSize) { + Point previewSize = convertSizeToLandscapeOrientation(new Point(getWidth(), getHeight())); + float cameraRatio = ((float) cameraSize.width) / cameraSize.height; + float screenRatio = ((float) previewSize.x) / previewSize.y; + + if (screenRatio > cameraRatio) { + setViewSize((int) (previewSize.y * cameraRatio), previewSize.y); + } else { + setViewSize(previewSize.x, (int) (previewSize.x / cameraRatio)); + } + } + + @SuppressWarnings("SuspiciousNameCombination") + private Point convertSizeToLandscapeOrientation(Point size) { + if (getDisplayOrientation() % 180 == 0) { + return size; + } else { + return new Point(size.y, size.x); + } + } + + @SuppressWarnings("SuspiciousNameCombination") + private void setViewSize(int width, int height) { + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + int tmpWidth; + int tmpHeight; + if (getDisplayOrientation() % 180 == 0) { + tmpWidth = width; + tmpHeight = height; + } else { + tmpWidth = height; + tmpHeight = width; + } + + if (mShouldScaleToFill) { + int parentWidth = ((View) getParent()).getWidth(); + int parentHeight = ((View) getParent()).getHeight(); + float ratioWidth = (float) parentWidth / (float) tmpWidth; + float ratioHeight = (float) parentHeight / (float) tmpHeight; + + float compensation; + + if (ratioWidth > ratioHeight) { + compensation = ratioWidth; + } else { + compensation = ratioHeight; + } + + tmpWidth = Math.round(tmpWidth * compensation); + tmpHeight = Math.round(tmpHeight * compensation); + } + + layoutParams.width = tmpWidth; + layoutParams.height = tmpHeight; + setLayoutParams(layoutParams); + } + + public int getDisplayOrientation() { + if (mCameraWrapper == null) { + //If we don't have a camera set there is no orientation so return dummy value + return 0; + } + + Camera.CameraInfo info = new Camera.CameraInfo(); + if(mCameraWrapper.mCameraId == -1) { + Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info); + } else { + Camera.getCameraInfo(mCameraWrapper.mCameraId, info); + } + + WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + int rotation = display.getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + } + + int result; + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + private Camera.Size getOptimalPreviewSize() { + if(mCameraWrapper == null) { + return null; + } + + List sizes = mCameraWrapper.mCamera.getParameters().getSupportedPreviewSizes(); + int w = getWidth(); + int h = getHeight(); + if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) { + int portraitWidth = h; + h = w; + w = portraitWidth; + } + + double targetRatio = (double) w / h; + if (sizes == null) return null; + + Camera.Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + + int targetHeight = h; + + // Try to find an size match aspect ratio and size + for (Camera.Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > mAspectTolerance) continue; + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + + // Cannot find the one match the aspect ratio, ignore the requirement + if (optimalSize == null) { + minDiff = Double.MAX_VALUE; + for (Camera.Size size : sizes) { + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + } + return optimalSize; + } + + public void setAutoFocus(boolean state) { + if(mCameraWrapper != null && mPreviewing) { + if(state == mAutoFocus) { + return; + } + mAutoFocus = state; + if(mAutoFocus) { + if (mSurfaceCreated) { // check if surface created before using autofocus + Log.v(TAG, "Starting autofocus"); + safeAutoFocus(); + } else { + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } else { + Log.v(TAG, "Cancelling autofocus"); + mCameraWrapper.mCamera.cancelAutoFocus(); + } + } + } + + private Runnable doAutoFocus = new Runnable() { + public void run() { + if(mCameraWrapper != null && mPreviewing && mAutoFocus && mSurfaceCreated) { + safeAutoFocus(); + } + } + }; + + // Mimic continuous auto-focusing + Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() { + public void onAutoFocus(boolean success, Camera camera) { + scheduleAutoFocus(); + } + }; + + private void scheduleAutoFocus() { + mAutoFocusHandler.postDelayed(doAutoFocus, 1000); + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java new file mode 100644 index 0000000000..599bd5fa48 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java @@ -0,0 +1,63 @@ +package me.dm7.barcodescanner.core; + +import android.hardware.Camera; + +import java.util.List; + +public class CameraUtils { + /** A safe way to get an instance of the Camera object. */ + public static Camera getCameraInstance() { + return getCameraInstance(getDefaultCameraId()); + } + + /** Favor back-facing camera by default. If none exists, fallback to whatever camera is available **/ + public static int getDefaultCameraId() { + int numberOfCameras = Camera.getNumberOfCameras(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + int defaultCameraId = -1; + for (int i = 0; i < numberOfCameras; i++) { + defaultCameraId = i; + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { + return i; + } + } + return defaultCameraId; + } + + /** A safe way to get an instance of the Camera object. */ + public static Camera getCameraInstance(int cameraId) { + Camera c = null; + try { + if(cameraId == -1) { + c = Camera.open(); // attempt to get a Camera instance + } else { + c = Camera.open(cameraId); // attempt to get a Camera instance + } + } + catch (Exception e) { + // Camera is not available (in use or does not exist) + } + return c; // returns null if camera is unavailable + } + + public static boolean isFlashSupported(Camera camera) { + /* Credits: Top answer at http://stackoverflow.com/a/19599365/868173 */ + if (camera != null) { + Camera.Parameters parameters = camera.getParameters(); + + if (parameters.getFlashMode() == null) { + return false; + } + + List supportedFlashModes = parameters.getSupportedFlashModes(); + if (supportedFlashModes == null || supportedFlashModes.isEmpty() || supportedFlashModes.size() == 1 && supportedFlashModes.get(0).equals(Camera.Parameters.FLASH_MODE_OFF)) { + return false; + } + } else { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java new file mode 100644 index 0000000000..49759ae0f9 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java @@ -0,0 +1,23 @@ +package me.dm7.barcodescanner.core; + +import android.hardware.Camera; + +import androidx.annotation.NonNull; + +public class CameraWrapper { + public final Camera mCamera; + public final int mCameraId; + + private CameraWrapper(@NonNull Camera camera, int cameraId) { + this.mCamera = camera; + this.mCameraId = cameraId; + } + + public static CameraWrapper getWrapper(Camera camera, int cameraId) { + if (camera == null) { + return null; + } else { + return new CameraWrapper(camera, cameraId); + } + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java new file mode 100644 index 0000000000..6c47312221 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java @@ -0,0 +1,41 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.view.Display; +import android.view.WindowManager; + +public class DisplayUtils { + public static Point getScreenResolution(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point screenResolution = new Point(); + if (android.os.Build.VERSION.SDK_INT >= 13) { + display.getSize(screenResolution); + } else { + screenResolution.set(display.getWidth(), display.getHeight()); + } + + return screenResolution; + } + + public static int getScreenOrientation(Context context) + { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + int orientation = Configuration.ORIENTATION_UNDEFINED; + if(display.getWidth()==display.getHeight()){ + orientation = Configuration.ORIENTATION_SQUARE; + } else{ + if(display.getWidth() < display.getHeight()){ + orientation = Configuration.ORIENTATION_PORTRAIT; + }else { + orientation = Configuration.ORIENTATION_LANDSCAPE; + } + } + return orientation; + } + +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java new file mode 100644 index 0000000000..a6caaa4cb6 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java @@ -0,0 +1,53 @@ +package me.dm7.barcodescanner.core; + +import android.graphics.Rect; + +public interface IViewFinder { + + void setLaserColor(int laserColor); + void setMaskColor(int maskColor); + void setBorderColor(int borderColor); + void setBorderStrokeWidth(int borderStrokeWidth); + void setBorderLineLength(int borderLineLength); + void setLaserEnabled(boolean isLaserEnabled); + + void setBorderCornerRounded(boolean isBorderCornersRounded); + void setBorderAlpha(float alpha); + void setBorderCornerRadius(int borderCornersRadius); + void setViewFinderOffset(int offset); + void setSquareViewFinder(boolean isSquareViewFinder); + /** + * Method that executes when Camera preview is starting. + * It is recommended to update framing rect here and invalidate view after that.
+ * For example see: {@link ViewFinderView#setupViewFinder()} + */ + void setupViewFinder(); + + /** + * Provides {@link Rect} that identifies area where barcode scanner can detect visual codes + *

Note: This rect is a area representation in absolute pixel values.
+ * For example:
+ * If View's size is 1024x800 so framing rect might be 500x400

+ * + * @return {@link Rect} that identifies barcode scanner area + */ + Rect getFramingRect(); + + /** + * Width of a {@link android.view.View} that implements this interface + *

Note: this is already implemented in {@link android.view.View}, + * so you don't need to override method and provide your implementation

+ * + * @return width of a view + */ + int getWidth(); + + /** + * Height of a {@link android.view.View} that implements this interface + *

Note: this is already implemented in {@link android.view.View}, + * so you don't need to override method and provide your implementation

+ * + * @return height of a view + */ + int getHeight(); +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java new file mode 100644 index 0000000000..307a8a42b4 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java @@ -0,0 +1,259 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +public class ViewFinderView extends View implements IViewFinder { + private static final String TAG = "ViewFinderView"; + + private Rect mFramingRect; + + private static final float PORTRAIT_WIDTH_RATIO = 6f/8; + private static final float PORTRAIT_WIDTH_HEIGHT_RATIO = 0.75f; + + private static final float LANDSCAPE_HEIGHT_RATIO = 5f/8; + private static final float LANDSCAPE_WIDTH_HEIGHT_RATIO = 1.4f; + private static final int MIN_DIMENSION_DIFF = 50; + + private static final float DEFAULT_SQUARE_DIMENSION_RATIO = 5f / 8; + + private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64}; + private int scannerAlpha; + private static final int POINT_SIZE = 10; + private static final long ANIMATION_DELAY = 80l; + + private final int mDefaultLaserColor = getResources().getColor(R.color.viewfinder_laser); + private final int mDefaultMaskColor = getResources().getColor(R.color.viewfinder_mask); + private final int mDefaultBorderColor = getResources().getColor(R.color.viewfinder_border); + private final int mDefaultBorderStrokeWidth = getResources().getInteger(R.integer.viewfinder_border_width); + private final int mDefaultBorderLineLength = getResources().getInteger(R.integer.viewfinder_border_length); + + protected Paint mLaserPaint; + protected Paint mFinderMaskPaint; + protected Paint mBorderPaint; + protected int mBorderLineLength; + protected boolean mSquareViewFinder; + private boolean mIsLaserEnabled; + private float mBordersAlpha; + private int mViewFinderOffset = 0; + + public ViewFinderView(Context context) { + super(context); + init(); + } + + public ViewFinderView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + init(); + } + + private void init() { + //set up laser paint + mLaserPaint = new Paint(); + mLaserPaint.setColor(mDefaultLaserColor); + mLaserPaint.setStyle(Paint.Style.FILL); + + //finder mask paint + mFinderMaskPaint = new Paint(); + mFinderMaskPaint.setColor(mDefaultMaskColor); + + //border paint + mBorderPaint = new Paint(); + mBorderPaint.setColor(mDefaultBorderColor); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setStrokeWidth(mDefaultBorderStrokeWidth); + mBorderPaint.setAntiAlias(true); + + mBorderLineLength = mDefaultBorderLineLength; + } + + @Override + public void setLaserColor(int laserColor) { + mLaserPaint.setColor(laserColor); + } + + @Override + public void setMaskColor(int maskColor) { + mFinderMaskPaint.setColor(maskColor); + } + + @Override + public void setBorderColor(int borderColor) { + mBorderPaint.setColor(borderColor); + } + + @Override + public void setBorderStrokeWidth(int borderStrokeWidth) { + mBorderPaint.setStrokeWidth(borderStrokeWidth); + } + + @Override + public void setBorderLineLength(int borderLineLength) { + mBorderLineLength = borderLineLength; + } + + @Override + public void setLaserEnabled(boolean isLaserEnabled) { mIsLaserEnabled = isLaserEnabled; } + + @Override + public void setBorderCornerRounded(boolean isBorderCornersRounded) { + if (isBorderCornersRounded) { + mBorderPaint.setStrokeJoin(Paint.Join.ROUND); + } else { + mBorderPaint.setStrokeJoin(Paint.Join.BEVEL); + } + } + + @Override + public void setBorderAlpha(float alpha) { + int colorAlpha = (int) (255 * alpha); + mBordersAlpha = alpha; + mBorderPaint.setAlpha(colorAlpha); + } + + @Override + public void setBorderCornerRadius(int borderCornersRadius) { + mBorderPaint.setPathEffect(new CornerPathEffect(borderCornersRadius)); + } + + @Override + public void setViewFinderOffset(int offset) { + mViewFinderOffset = offset; + } + + // TODO: Need a better way to configure this. Revisit when working on 2.0 + @Override + public void setSquareViewFinder(boolean set) { + mSquareViewFinder = set; + } + + public void setupViewFinder() { + updateFramingRect(); + invalidate(); + } + + public Rect getFramingRect() { + return mFramingRect; + } + + @Override + public void onDraw(Canvas canvas) { + if(getFramingRect() == null) { + return; + } + + drawViewFinderMask(canvas); + drawViewFinderBorder(canvas); + + if (mIsLaserEnabled) { + drawLaser(canvas); + } + } + + public void drawViewFinderMask(Canvas canvas) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + Rect framingRect = getFramingRect(); + + canvas.drawRect(0, 0, width, framingRect.top, mFinderMaskPaint); + canvas.drawRect(0, framingRect.top, framingRect.left, framingRect.bottom + 1, mFinderMaskPaint); + canvas.drawRect(framingRect.right + 1, framingRect.top, width, framingRect.bottom + 1, mFinderMaskPaint); + canvas.drawRect(0, framingRect.bottom + 1, width, height, mFinderMaskPaint); + } + + public void drawViewFinderBorder(Canvas canvas) { + Rect framingRect = getFramingRect(); + + // Top-left corner + Path path = new Path(); + path.moveTo(framingRect.left, framingRect.top + mBorderLineLength); + path.lineTo(framingRect.left, framingRect.top); + path.lineTo(framingRect.left + mBorderLineLength, framingRect.top); + canvas.drawPath(path, mBorderPaint); + + // Top-right corner + path.moveTo(framingRect.right, framingRect.top + mBorderLineLength); + path.lineTo(framingRect.right, framingRect.top); + path.lineTo(framingRect.right - mBorderLineLength, framingRect.top); + canvas.drawPath(path, mBorderPaint); + + // Bottom-right corner + path.moveTo(framingRect.right, framingRect.bottom - mBorderLineLength); + path.lineTo(framingRect.right, framingRect.bottom); + path.lineTo(framingRect.right - mBorderLineLength, framingRect.bottom); + canvas.drawPath(path, mBorderPaint); + + // Bottom-left corner + path.moveTo(framingRect.left, framingRect.bottom - mBorderLineLength); + path.lineTo(framingRect.left, framingRect.bottom); + path.lineTo(framingRect.left + mBorderLineLength, framingRect.bottom); + canvas.drawPath(path, mBorderPaint); + } + + public void drawLaser(Canvas canvas) { + Rect framingRect = getFramingRect(); + + // Draw a red "laser scanner" line through the middle to show decoding is active + mLaserPaint.setAlpha(SCANNER_ALPHA[scannerAlpha]); + scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length; + int middle = framingRect.height() / 2 + framingRect.top; + canvas.drawRect(framingRect.left + 2, middle - 1, framingRect.right - 1, middle + 2, mLaserPaint); + + postInvalidateDelayed(ANIMATION_DELAY, + framingRect.left - POINT_SIZE, + framingRect.top - POINT_SIZE, + framingRect.right + POINT_SIZE, + framingRect.bottom + POINT_SIZE); + } + + @Override + protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { + updateFramingRect(); + } + + public synchronized void updateFramingRect() { + Point viewResolution = new Point(getWidth(), getHeight()); + int width; + int height; + int orientation = DisplayUtils.getScreenOrientation(getContext()); + + if(mSquareViewFinder) { + if(orientation != Configuration.ORIENTATION_PORTRAIT) { + height = (int) (getHeight() * DEFAULT_SQUARE_DIMENSION_RATIO); + width = height; + } else { + width = (int) (getWidth() * DEFAULT_SQUARE_DIMENSION_RATIO); + height = width; + } + } else { + if(orientation != Configuration.ORIENTATION_PORTRAIT) { + height = (int) (getHeight() * LANDSCAPE_HEIGHT_RATIO); + width = (int) (LANDSCAPE_WIDTH_HEIGHT_RATIO * height); + } else { + width = (int) (getWidth() * PORTRAIT_WIDTH_RATIO); + height = (int) (PORTRAIT_WIDTH_HEIGHT_RATIO * width); + } + } + + if(width > getWidth()) { + width = getWidth() - MIN_DIMENSION_DIFF; + } + + if(height > getHeight()) { + height = getHeight() - MIN_DIMENSION_DIFF; + } + + int leftOffset = (viewResolution.x - width) / 2; + int topOffset = (viewResolution.y - height) / 2; + mFramingRect = new Rect(leftOffset + mViewFinderOffset, topOffset + mViewFinderOffset, leftOffset + width - mViewFinderOffset, topOffset + height - mViewFinderOffset); + } +} + diff --git a/library/external/barcodescanner/core/src/main/res/values-hdpi/integers.xml b/library/external/barcodescanner/core/src/main/res/values-hdpi/integers.xml new file mode 100644 index 0000000000..d5979caaf8 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-hdpi/integers.xml @@ -0,0 +1,5 @@ + + + 4 + 60 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values-xhdpi/integers.xml b/library/external/barcodescanner/core/src/main/res/values-xhdpi/integers.xml new file mode 100644 index 0000000000..7f57b00a3a --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-xhdpi/integers.xml @@ -0,0 +1,5 @@ + + + 5 + 80 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values-xxhdpi/integers.xml b/library/external/barcodescanner/core/src/main/res/values-xxhdpi/integers.xml new file mode 100644 index 0000000000..d69e2a8b0f --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-xxhdpi/integers.xml @@ -0,0 +1,5 @@ + + + 6 + 100 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values/attrs.xml b/library/external/barcodescanner/core/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..8e7485ebb3 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/attrs.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/library/external/barcodescanner/core/src/main/res/values/colors.xml b/library/external/barcodescanner/core/src/main/res/values/colors.xml new file mode 100644 index 0000000000..58c019a6a9 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #60000000 + #ffcc0000 + #ffafed44 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values/integers.xml b/library/external/barcodescanner/core/src/main/res/values/integers.xml new file mode 100644 index 0000000000..690b6cd3c9 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 4 + 60 + diff --git a/library/external/barcodescanner/zxing/build.gradle b/library/external/barcodescanner/zxing/build.gradle new file mode 100644 index 0000000000..e84d0f5bb9 --- /dev/null +++ b/library/external/barcodescanner/zxing/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + namespace "me.dm7.barcodescanner.zxing" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +dependencies { + api project(":library:external:barcodescanner:core") + // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 + api 'com.google.zxing:core:3.3.3' +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java b/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java new file mode 100644 index 0000000000..d1717ba5be --- /dev/null +++ b/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java @@ -0,0 +1,198 @@ +package me.dm7.barcodescanner.zxing; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.Camera; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import me.dm7.barcodescanner.core.BarcodeScannerView; +import me.dm7.barcodescanner.core.DisplayUtils; + +public class ZXingScannerView extends BarcodeScannerView { + private static final String TAG = "ZXingScannerView"; + + public interface ResultHandler { + void handleResult(Result rawResult); + } + + private MultiFormatReader mMultiFormatReader; + public static final List ALL_FORMATS = new ArrayList<>(); + private List mFormats; + private ResultHandler mResultHandler; + + static { + ALL_FORMATS.add(BarcodeFormat.AZTEC); + ALL_FORMATS.add(BarcodeFormat.CODABAR); + ALL_FORMATS.add(BarcodeFormat.CODE_39); + ALL_FORMATS.add(BarcodeFormat.CODE_93); + ALL_FORMATS.add(BarcodeFormat.CODE_128); + ALL_FORMATS.add(BarcodeFormat.DATA_MATRIX); + ALL_FORMATS.add(BarcodeFormat.EAN_8); + ALL_FORMATS.add(BarcodeFormat.EAN_13); + ALL_FORMATS.add(BarcodeFormat.ITF); + ALL_FORMATS.add(BarcodeFormat.MAXICODE); + ALL_FORMATS.add(BarcodeFormat.PDF_417); + ALL_FORMATS.add(BarcodeFormat.QR_CODE); + ALL_FORMATS.add(BarcodeFormat.RSS_14); + ALL_FORMATS.add(BarcodeFormat.RSS_EXPANDED); + ALL_FORMATS.add(BarcodeFormat.UPC_A); + ALL_FORMATS.add(BarcodeFormat.UPC_E); + ALL_FORMATS.add(BarcodeFormat.UPC_EAN_EXTENSION); + } + + public ZXingScannerView(Context context) { + super(context); + initMultiFormatReader(); + } + + public ZXingScannerView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + initMultiFormatReader(); + } + + public void setFormats(List formats) { + mFormats = formats; + initMultiFormatReader(); + } + + public void setResultHandler(ResultHandler resultHandler) { + mResultHandler = resultHandler; + } + + public Collection getFormats() { + if(mFormats == null) { + return ALL_FORMATS; + } + return mFormats; + } + + private void initMultiFormatReader() { + Map hints = new EnumMap<>(DecodeHintType.class); + hints.put(DecodeHintType.POSSIBLE_FORMATS, getFormats()); + mMultiFormatReader = new MultiFormatReader(); + mMultiFormatReader.setHints(hints); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if(mResultHandler == null) { + return; + } + + try { + Camera.Parameters parameters = camera.getParameters(); + Camera.Size size = parameters.getPreviewSize(); + int width = size.width; + int height = size.height; + + if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) { + int rotationCount = getRotationCount(); + if (rotationCount == 1 || rotationCount == 3) { + int tmp = width; + width = height; + height = tmp; + } + data = getRotatedData(data, camera); + } + + Result rawResult = null; + PlanarYUVLuminanceSource source = buildLuminanceSource(data, width, height); + + if (source != null) { + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + try { + rawResult = mMultiFormatReader.decodeWithState(bitmap); + } catch (ReaderException re) { + // continue + } catch (NullPointerException npe) { + // This is terrible + } catch (ArrayIndexOutOfBoundsException aoe) { + + } finally { + mMultiFormatReader.reset(); + } + + if (rawResult == null) { + LuminanceSource invertedSource = source.invert(); + bitmap = new BinaryBitmap(new HybridBinarizer(invertedSource)); + try { + rawResult = mMultiFormatReader.decodeWithState(bitmap); + } catch (NotFoundException e) { + // continue + } finally { + mMultiFormatReader.reset(); + } + } + } + + final Result finalRawResult = rawResult; + + if (finalRawResult != null) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + // Stopping the preview can take a little long. + // So we want to set result handler to null to discard subsequent calls to + // onPreviewFrame. + ResultHandler tmpResultHandler = mResultHandler; + mResultHandler = null; + + stopCameraPreview(); + if (tmpResultHandler != null) { + tmpResultHandler.handleResult(finalRawResult); + } + } + }); + } else { + camera.setOneShotPreviewCallback(this); + } + } catch(RuntimeException e) { + // TODO: Terrible hack. It is possible that this method is invoked after camera is released. + Log.e(TAG, e.toString(), e); + } + } + + public void resumeCameraPreview(ResultHandler resultHandler) { + mResultHandler = resultHandler; + super.resumeCameraPreview(); + } + + public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) { + Rect rect = getFramingRectInPreview(width, height); + if (rect == null) { + return null; + } + // Go ahead and assume it's YUV rather than die. + PlanarYUVLuminanceSource source = null; + + try { + source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, + rect.width(), rect.height(), false); + } catch(Exception e) { + } + + return source; + } +} diff --git a/library/external/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle index 7b3b62c082..c501f583d0 100644 --- a/library/external/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -58,9 +58,7 @@ dependencies { implementation libs.airbnb.mavericks // Span utils - implementation('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle new file mode 100644 index 0000000000..e051550210 --- /dev/null +++ b/library/external/realmfieldnameshelper/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'kotlin' +apply plugin: 'java' + +sourceCompatibility = versions.sourceCompat +targetCompatibility = versions.sourceCompat + +dependencies { + implementation 'com.squareup:javapoet:1.13.0' +} + +task javadocJar(type: Jar, dependsOn: 'javadoc') { + from javadoc.destinationDir + classifier = 'javadoc' +} +task sourcesJar(type: Jar, dependsOn: 'classes') { + from sourceSets.main.allSource + classifier = 'sources' +} + +sourceSets { + main.java.srcDirs += 'src/main/kotlin' +} + diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt new file mode 100644 index 0000000000..d683a2adef --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt @@ -0,0 +1,24 @@ +package dk.ilios.realmfieldnames + +import java.util.TreeMap + +/** + * Class responsible for keeping track of the metadata for each Realm model class. + */ +class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) { + + val fields = TreeMap() // + + fun addField(field: String, linkedType: String?) { + fields.put(field, linkedType) + } + + val qualifiedClassName: String + get() { + if (packageName != null && !packageName.isEmpty()) { + return packageName + "." + simpleClassName + } else { + return simpleClassName + } + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt new file mode 100644 index 0000000000..95f0024721 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -0,0 +1,79 @@ +package dk.ilios.realmfieldnames + +import java.util.Locale + +/** + * Class for encapsulating the rules for converting between the field name in the Realm model class + * and the matching name in the "<class>Fields" class. + */ +class FieldNameFormatter { + + @JvmOverloads + fun format(fieldName: String?, locale: Locale = Locale.US): String { + if (fieldName == null || fieldName == "") { + return "" + } + + // Normalize word separator chars + val normalizedFieldName: String = fieldName.replace('-', '_') + + // Iterate field name using the following rules + // lowerCase m followed by upperCase anything is considered hungarian notation + // lowercase char followed by uppercase char is considered camel case + // Two uppercase chars following each other is considered non-standard camelcase + // _ and - are treated as word separators + val result = StringBuilder(normalizedFieldName.length) + + if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) { + result.append(normalizedFieldName) + } else { + var previousCodepoint: Int? + var currentCodepoint: Int? = null + val length = normalizedFieldName.length + var offset = 0 + while (offset < length) { + previousCodepoint = currentCodepoint + currentCodepoint = normalizedFieldName.codePointAt(offset) + + if (previousCodepoint != null) { + if (Character.isUpperCase(currentCodepoint) && + !Character.isUpperCase(previousCodepoint) && + previousCodepoint === 'm'.code as Int? && + result.length == 1 + ) { + // Hungarian notation starting with: mX + result.delete(0, 1) + result.appendCodePoint(currentCodepoint) + } else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) { + // InvalidCamelCase: XXYx (should have been xxYx) + if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) { + val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint)) + if (Character.isLowerCase(nextCodePoint)) { + result.append("_") + } + } + result.appendCodePoint(currentCodepoint) + } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { + // Word-separator: x-x or x_x + result.append("_") + } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit( + previousCodepoint + )) { + // camelCase: xX + result.append("_") + result.appendCodePoint(currentCodepoint) + } else { + // Unknown type + result.appendCodePoint(currentCodepoint) + } + } else { + // Only triggered for first code point + result.appendCodePoint(currentCodepoint) + } + offset += Character.charCount(currentCodepoint) + } + } + + return result.toString().uppercase(locale) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt new file mode 100644 index 0000000000..2ddba1ccbd --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -0,0 +1,77 @@ +package dk.ilios.realmfieldnames + +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import java.io.IOException +import javax.annotation.processing.Filer +import javax.lang.model.element.Modifier + +/** + * Class responsible for creating the final output files. + */ +class FileGenerator(private val filer: Filer) { + private val formatter: FieldNameFormatter + + init { + this.formatter = FieldNameFormatter() + } + + /** + * Generates all the "<class>Fields" fields with field name references. + * @param fileData Files to create. + * * + * @return `true` if the files where generated, `false` if not. + */ + fun generate(fileData: Set): Boolean { + return fileData + .filter { !it.libraryClass } + .all { generateFile(it, fileData) } + } + + private fun generateFile(classData: ClassData, classPool: Set): Boolean { + val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n", + classData.packageName, classData.simpleClassName) + + // Add a static field reference to each queryable field in the Realm model class + classData.fields.forEach { fieldName, value -> + if (value != null) { + // Add linked field names (only up to depth 1) + for (data in classPool) { + if (data.qualifiedClassName == value) { + val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) + val linkedClassFields = data.fields + addField(linkedTypeSpec, "$", fieldName) + for (linkedFieldName in linkedClassFields.keys) { + addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName) + } + fileBuilder.addType(linkedTypeSpec.build()) + } + } + } else { + // Add normal field name + addField(fileBuilder, fieldName, fieldName) + } + } + + val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build() + try { + javaFile.writeTo(filer) + return true + } catch (e: IOException) { + // e.printStackTrace() + return false + } + } + + private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) { + val field = FieldSpec.builder(String::class.java, formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("\$S", fieldNameValue) + .build() + fileBuilder.addField(field) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt new file mode 100644 index 0000000000..29d044c46c --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -0,0 +1,197 @@ +package dk.ilios.realmfieldnames + +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.Messager +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +/** + * The Realm Field Names Generator is a processor that looks at all available Realm model classes + * and create an companion class with easy, type-safe access to all field names. + */ + +@SupportedAnnotationTypes("io.realm.annotations.RealmClass") +class RealmFieldNamesProcessor : AbstractProcessor() { + + private val classes = HashSet() + private lateinit var typeUtils: Types + private lateinit var messager: Messager + private lateinit var elementUtils: Elements + private var ignoreAnnotation: TypeMirror? = null + private var realmClassAnnotation: TypeElement? = null + private var realmModelInterface: TypeMirror? = null + private var realmListClass: DeclaredType? = null + private var realmResultsClass: DeclaredType? = null + private var fileGenerator: FileGenerator? = null + private var done = false + + @Synchronized + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + typeUtils = processingEnv.typeUtils!! + messager = processingEnv.messager!! + elementUtils = processingEnv.elementUtils!! + + // If the Realm class isn't found something is wrong the project setup. + // Most likely Realm isn't on the class path, so just disable the + // annotation processor + val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null + if (!isRealmAvailable) { + done = true + } else { + ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType() + realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass") + realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType() + realmListClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmList"), + typeUtils.getWildcardType(null, null) + ) + realmResultsClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmResults"), + typeUtils.getWildcardType(null, null) + ) + fileGenerator = FileGenerator(processingEnv.filer) + } + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latestSupported() + } + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + if (done) { + return CONSUME_ANNOTATIONS + } + + // Create all proxy classes + roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement -> + if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) { + val classData = processClass(classElement as TypeElement) + classes.add(classData) + } + } + + // If a model class references a library class, the library class will not be part of this + // annotation processor round. For all those references we need to pull field information + // from the classpath instead. + val libraryClasses = HashMap() + classes.forEach { + it.fields.forEach { _, value -> + // Analyze the library class file the first time it is encountered. + if (value != null) { + if (classes.all { it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { + libraryClasses.put(value, processLibraryClass(value)) + } + } + } + } + classes.addAll(libraryClasses.values) + + done = fileGenerator!!.generate(classes) + return CONSUME_ANNOTATIONS + } + + private fun processClass(classElement: TypeElement): ClassData { + val packageName = getPackageName(classElement) + val className = classElement.simpleName.toString() + val data = ClassData(packageName, className) + + // Find all appropriate fields + classElement.enclosedElements.forEach { + val elementKind = it.kind + if (elementKind == ElementKind.FIELD) { + val variableElement = it as VariableElement + + val modifiers = variableElement.modifiers + if (modifiers.contains(Modifier.STATIC)) { + return@forEach // completely ignore any static fields + } + + // Don't add any fields marked with @Ignore + val ignoreField = variableElement.annotationMirrors + .map { it.annotationType.toString() } + .contains("io.realm.annotations.Ignore") + + if (!ignoreField) { + data.addField(it.getSimpleName().toString(), getLinkedFieldType(it)) + } + } + } + + return data + } + + private fun processLibraryClass(qualifiedClassName: String): ClassData { + val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath + val packageName = libraryClass.`package`.name + val className = libraryClass.simpleName + val data = ClassData(packageName, className, libraryClass = true) + + libraryClass.declaredFields.forEach { field -> + if (java.lang.reflect.Modifier.isStatic(field.modifiers)) { + return@forEach // completely ignore any static fields + } + + // Add field if it is not being ignored. + if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) { + data.addField(field.name, field.type.name) + } + } + + return data + } + + /** + * Returns the qualified name of the linked Realm class field or `null` if it is not a linked + * class. + */ + private fun getLinkedFieldType(field: Element): String? { + if (typeUtils.isAssignable(field.asType(), realmModelInterface)) { + // Object link + val typeElement = elementUtils.getTypeElement(field.asType().toString()) + return typeElement.qualifiedName.toString() + } else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) { + // List link or LinkingObjects + val fieldType = field.asType() + val typeArguments = (fieldType as DeclaredType).typeArguments + if (typeArguments.size == 0) { + return null + } + return typeArguments[0].toString() + } else { + return null + } + } + + private fun getPackageName(classElement: TypeElement): String? { + val enclosingElement = classElement.enclosingElement + + if (enclosingElement.kind != ElementKind.PACKAGE) { + messager.printMessage( + Diagnostic.Kind.ERROR, + "Could not determine the package name. Enclosing element was: " + enclosingElement.kind + ) + return null + } + + val packageElement = enclosingElement as PackageElement + return packageElement.qualifiedName.toString() + } + + companion object { + private const val CONSUME_ANNOTATIONS = false + } +} diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 0000000000..57897c8297 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..58fadd699c --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor \ No newline at end of file diff --git a/library/external/span/build.gradle b/library/external/span/build.gradle new file mode 100644 index 0000000000..05adbacb4d --- /dev/null +++ b/library/external/span/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "me.gujun.android.span" + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + implementation 'com.android.support:support-annotations:28.0.0' +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/Span.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/Span.kt new file mode 100644 index 0000000000..5ca63b7e01 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/Span.kt @@ -0,0 +1,316 @@ +package me.gujun.android.span + +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.Layout +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.text.style.AbsoluteSizeSpan +import android.text.style.AlignmentSpan +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.ImageSpan +import android.text.style.QuoteSpan +import android.text.style.StyleSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import me.gujun.android.span.style.CustomTypefaceSpan +import me.gujun.android.span.style.LineSpacingSpan +import me.gujun.android.span.style.SimpleClickableSpan +import me.gujun.android.span.style.TextDecorationLineSpan +import me.gujun.android.span.style.VerticalPaddingSpan + +class Span(val parent: Span? = null) : SpannableStringBuilder() { + + companion object { + val EMPTY_STYLE = Span() + + var globalStyle: Span = EMPTY_STYLE + } + + var text: CharSequence = "" + + @ColorInt var textColor: Int? = parent?.textColor + + @ColorInt var backgroundColor: Int? = parent?.backgroundColor + + @Dimension(unit = Dimension.PX) var textSize: Int? = parent?.textSize + + var fontFamily: String? = parent?.fontFamily + + var typeface: Typeface? = parent?.typeface + + var textStyle: String? = parent?.textStyle + + var alignment: String? = parent?.alignment + + var textDecorationLine: String? = parent?.textDecorationLine + + @Dimension(unit = Dimension.PX) var lineSpacing: Int? = null + + @Dimension(unit = Dimension.PX) var paddingTop: Int? = null + + @Dimension(unit = Dimension.PX) var paddingBottom: Int? = null + + @Dimension(unit = Dimension.PX) var verticalPadding: Int? = null + + var onClick: (() -> Unit)? = null + + var spans: ArrayList = ArrayList() + + var style: Span = EMPTY_STYLE + + private fun buildCharacterStyle(builder: ArrayList) { + if (textColor != null) { + builder.add(ForegroundColorSpan(textColor!!)) + } + + if (backgroundColor != null) { + builder.add(BackgroundColorSpan(backgroundColor!!)) + } + + if (textSize != null) { + builder.add(AbsoluteSizeSpan(textSize!!)) + } + + if (!TextUtils.isEmpty(fontFamily)) { + builder.add(TypefaceSpan(fontFamily)) + } + + if (typeface != null) { + builder.add(CustomTypefaceSpan(typeface!!)) + } + + if (!TextUtils.isEmpty(textStyle)) { + builder.add(StyleSpan(when (textStyle) { + "normal" -> Typeface.NORMAL + "bold" -> Typeface.BOLD + "italic" -> Typeface.ITALIC + "bold_italic" -> Typeface.BOLD_ITALIC + else -> throw RuntimeException("Unknown text style") + })) + } + + if (!TextUtils.isEmpty(textDecorationLine)) { + builder.add(TextDecorationLineSpan(textDecorationLine!!)) + } + + if (onClick != null) { + builder.add(object : SimpleClickableSpan() { + override fun onClick(widget: View) { + onClick?.invoke() + } + }) + } + } + + private fun buildParagraphStyle(builder: ArrayList) { + if (!TextUtils.isEmpty(alignment)) { + builder.add(AlignmentSpan.Standard(when (alignment) { + "normal" -> Layout.Alignment.ALIGN_NORMAL + "opposite" -> Layout.Alignment.ALIGN_OPPOSITE + "center" -> Layout.Alignment.ALIGN_CENTER + else -> throw RuntimeException("Unknown text alignment") + })) + } + + if (lineSpacing != null) { + builder.add(LineSpacingSpan(lineSpacing!!)) + } + + paddingTop = when { + paddingTop != null -> paddingTop + verticalPadding != null -> verticalPadding + else -> 0 + } + paddingBottom = when { + paddingBottom != null -> paddingBottom + verticalPadding != null -> verticalPadding + else -> 0 + } + if (paddingTop != 0 || paddingBottom != 0) { + builder.add(VerticalPaddingSpan(paddingTop!!, paddingBottom!!)) + } + } + + private fun prebuild() { + override(style) + } + + fun build(): Span { + prebuild() + val builder = ArrayList() + if (!TextUtils.isEmpty(text)) { + var p = this.parent + while (p != null) { + if (!TextUtils.isEmpty(p.text)) { + throw RuntimeException("Can't nest \"$text\" in spans") + } + p = p.parent + } + append(text) + buildCharacterStyle(builder) + buildParagraphStyle(builder) + } else { + buildParagraphStyle(builder) + } + + builder.addAll(spans) + builder.forEach { + setSpan(it, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this + } + + fun override(style: Span) { + if (textColor == null) { + textColor = style.textColor + } + if (backgroundColor == null) { + backgroundColor = style.backgroundColor + } + if (textSize == null) { + textSize = style.textSize + } + if (fontFamily == null) { + fontFamily = style.fontFamily + } + if (typeface == null) { + typeface = style.typeface + } + if (textStyle == null) { + textStyle = style.textStyle + } + if (alignment == null) { + alignment = style.alignment + } + if (textDecorationLine == null) { + textDecorationLine = style.textDecorationLine + } + if (lineSpacing == null) { + lineSpacing = style.lineSpacing + } + if (paddingTop == null) { + paddingTop = style.paddingTop + } + if (paddingBottom == null) { + paddingBottom = style.paddingBottom + } + if (verticalPadding == null) { + verticalPadding = style.verticalPadding + } + if (onClick == null) { + onClick = style.onClick + } + spans.addAll(style.spans) + } + + operator fun CharSequence.unaryPlus(): CharSequence { + return append(Span(parent = this@Span).apply { + text = this@unaryPlus + build() + }) + } + + operator fun Span.plus(other: CharSequence): CharSequence { + return append(Span(parent = this).apply { + text = other + build() + }) + } +} + +fun span(init: Span.() -> Unit): Span = Span().apply { + override(Span.globalStyle) + init() + build() +} + +fun span(text: CharSequence, init: Span.() -> Unit): Span = Span().apply { + override(Span.globalStyle) + this.text = text + init() + build() +} + +fun style(init: Span.() -> Unit): Span = Span().apply { + init() +} + +fun Span.span(init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + init() + build() + }) +} + +fun Span.span(text: CharSequence, init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + init() + build() + }) +} + +fun Span.link(url: String, text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(URLSpan(url)) + init() + build() + }) +} + +fun Span.quote(@ColorInt color: Int, text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(QuoteSpan(color)) + init() + build() + }) +} + +fun Span.superscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(SuperscriptSpan()) + init() + build() + }) +} + +fun Span.subscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(SubscriptSpan()) + init() + build() + }) +} + +fun Span.image(drawable: Drawable, alignment: String = "bottom", + init: Span.() -> Unit = {}): Span = apply { + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + append(Span(parent = this).apply { + this.text = " " + this.spans.add(ImageSpan(drawable, when (alignment) { + "bottom" -> ImageSpan.ALIGN_BOTTOM + "baseline" -> ImageSpan.ALIGN_BASELINE + else -> throw RuntimeException("Unknown image alignment") + })) + init() + build() + }) +} + +fun Span.addSpan(what: Any) = apply { + this.spans.add(what) +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt new file mode 100644 index 0000000000..c737c5bf11 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt @@ -0,0 +1,36 @@ +package me.gujun.android.span.style + +import android.graphics.Paint +import android.graphics.Typeface +import android.text.TextPaint +import android.text.style.MetricAffectingSpan + +class CustomTypefaceSpan(private val tf: Typeface) : MetricAffectingSpan() { + + override fun updateMeasureState(paint: TextPaint) { + apply(paint, tf) + } + + override fun updateDrawState(ds: TextPaint) { + apply(ds, tf) + } + + private fun apply(paint: Paint, tf: Typeface) { + val oldStyle: Int + + val old = paint.typeface + oldStyle = old?.style ?: 0 + + val fake = oldStyle and tf.style.inv() + + if (fake and Typeface.BOLD != 0) { + paint.isFakeBoldText = true + } + + if (fake and Typeface.ITALIC != 0) { + paint.textSkewX = -0.25f + } + + paint.typeface = tf + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt new file mode 100644 index 0000000000..53aa42aa83 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt @@ -0,0 +1,31 @@ +package me.gujun.android.span.style + +import android.graphics.Paint.FontMetricsInt +import android.text.Spanned +import android.text.style.LineHeightSpan + +class LineSpacingSpan(private val add: Int) : LineHeightSpan { + + override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, + fm: FontMetricsInt) { + text as Spanned + /*val spanStart =*/ text.getSpanStart(this) + val spanEnd = text.getSpanEnd(this) + +// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character +// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#")) +// Log.d("DEBUG", "LineSpacingSpan: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv") +// Log.d("DEBUG", "$fm") +// Log.d("DEBUG", "-----------------------") + + if (spanstartv == v) { + fm.descent += add + } else if (text[start - 1] == '\n') { + fm.descent += add + } + + if (end == spanEnd || end - 1 == spanEnd) { + fm.descent -= add + } + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt new file mode 100644 index 0000000000..2ae524bca9 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt @@ -0,0 +1,10 @@ +package me.gujun.android.span.style + +import android.text.TextPaint +import android.text.style.ClickableSpan + +abstract class SimpleClickableSpan : ClickableSpan() { + override fun updateDrawState(ds: TextPaint) { + // no-op + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt new file mode 100644 index 0000000000..ce37c138f7 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt @@ -0,0 +1,29 @@ +package me.gujun.android.span.style + +import android.text.TextPaint +import android.text.style.CharacterStyle + +class TextDecorationLineSpan(private val textDecorationLine: String) : CharacterStyle() { + + override fun updateDrawState(tp: TextPaint) { + when (textDecorationLine) { + "none" -> { + tp.isUnderlineText = false + tp.isStrikeThruText = false + } + "underline" -> { + tp.isUnderlineText = true + tp.isStrikeThruText = false + } + "line-through" -> { + tp.isUnderlineText = false + tp.isStrikeThruText = true + } + "underline line-through" -> { + tp.isUnderlineText = true + tp.isStrikeThruText = true + } + else -> throw RuntimeException("Unknown text decoration line") + } + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt new file mode 100644 index 0000000000..600ea72e57 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt @@ -0,0 +1,41 @@ +package me.gujun.android.span.style + +import android.graphics.Paint.FontMetricsInt +import android.text.Spanned +import android.text.style.LineHeightSpan + +class VerticalPaddingSpan(private val paddingTop: Int, + private val paddingBottom: Int) : LineHeightSpan { + + private var flag: Boolean = true + + override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, + fm: FontMetricsInt) { + text as Spanned + /*val spanStart =*/ text.getSpanStart(this) + val spanEnd = text.getSpanEnd(this) + +// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character +// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#")) +// Log.d("DEBUG", "VerticalPadding: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv") +// Log.d("DEBUG", "$fm") +// Log.d("DEBUG", "-----------------------") + + if (spanstartv == v) { + fm.top -= paddingTop + fm.ascent -= paddingTop + flag = true + } else if (flag && text[start - 1] != '\n') { + fm.top += paddingTop + fm.ascent += paddingTop + flag = false + } else { + flag = false + } + + if (end == spanEnd || end - 1 == spanEnd) { + fm.descent += paddingBottom + fm.bottom += paddingBottom + } + } +} diff --git a/library/external/textdrawable/build.gradle b/library/external/textdrawable/build.gradle new file mode 100644 index 0000000000..5eb27bf6aa --- /dev/null +++ b/library/external/textdrawable/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +apply plugin: 'com.android.library' + +android { + namespace "com.amulyakhare.textdrawable" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java b/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java new file mode 100644 index 0000000000..db42f8b764 --- /dev/null +++ b/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java @@ -0,0 +1,316 @@ +package com.amulyakhare.textdrawable; + +import android.graphics.*; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.graphics.drawable.shapes.RectShape; +import android.graphics.drawable.shapes.RoundRectShape; + +/** + * @author amulya + * @datetime 14 Oct 2014, 3:53 PM + */ +public class TextDrawable extends ShapeDrawable { + + private final Paint textPaint; + private final Paint borderPaint; + private static final float SHADE_FACTOR = 0.9f; + private final String text; + private final int color; + private final RectShape shape; + private final int height; + private final int width; + private final int fontSize; + private final float radius; + private final int borderThickness; + + private TextDrawable(Builder builder) { + super(builder.shape); + + // shape properties + shape = builder.shape; + height = builder.height; + width = builder.width; + radius = builder.radius; + + // text and color + text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text; + color = builder.color; + + // text paint settings + fontSize = builder.fontSize; + textPaint = new Paint(); + textPaint.setColor(builder.textColor); + textPaint.setAntiAlias(true); + textPaint.setFakeBoldText(builder.isBold); + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTypeface(builder.font); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setStrokeWidth(builder.borderThickness); + + // border paint settings + borderThickness = builder.borderThickness; + borderPaint = new Paint(); + borderPaint.setColor(getDarkerShade(color)); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(borderThickness); + + // drawable paint color + Paint paint = getPaint(); + paint.setColor(color); + + } + + private int getDarkerShade(int color) { + return Color.rgb((int)(SHADE_FACTOR * Color.red(color)), + (int)(SHADE_FACTOR * Color.green(color)), + (int)(SHADE_FACTOR * Color.blue(color))); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + Rect r = getBounds(); + + + // draw border + if (borderThickness > 0) { + drawBorder(canvas); + } + + int count = canvas.save(); + canvas.translate(r.left, r.top); + + // draw text + int width = this.width < 0 ? r.width() : this.width; + int height = this.height < 0 ? r.height() : this.height; + int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize; + textPaint.setTextSize(fontSize); + canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint); + + canvas.restoreToCount(count); + + } + + private void drawBorder(Canvas canvas) { + RectF rect = new RectF(getBounds()); + rect.inset(borderThickness/2, borderThickness/2); + + if (shape instanceof OvalShape) { + canvas.drawOval(rect, borderPaint); + } + else if (shape instanceof RoundRectShape) { + canvas.drawRoundRect(rect, radius, radius, borderPaint); + } + else { + canvas.drawRect(rect, borderPaint); + } + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + textPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getIntrinsicWidth() { + return width; + } + + @Override + public int getIntrinsicHeight() { + return height; + } + + public static IShapeBuilder builder() { + return new Builder(); + } + + public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder { + + private String text; + + private int color; + + private int borderThickness; + + private int width; + + private int height; + + private Typeface font; + + private RectShape shape; + + public int textColor; + + private int fontSize; + + private boolean isBold; + + private boolean toUpperCase; + + public float radius; + + private Builder() { + text = ""; + color = Color.GRAY; + textColor = Color.WHITE; + borderThickness = 0; + width = -1; + height = -1; + shape = new RectShape(); + font = Typeface.create("sans-serif-light", Typeface.NORMAL); + fontSize = -1; + isBold = false; + toUpperCase = false; + } + + public IConfigBuilder width(int width) { + this.width = width; + return this; + } + + public IConfigBuilder height(int height) { + this.height = height; + return this; + } + + public IConfigBuilder textColor(int color) { + this.textColor = color; + return this; + } + + public IConfigBuilder withBorder(int thickness) { + this.borderThickness = thickness; + return this; + } + + public IConfigBuilder useFont(Typeface font) { + this.font = font; + return this; + } + + public IConfigBuilder fontSize(int size) { + this.fontSize = size; + return this; + } + + public IConfigBuilder bold() { + this.isBold = true; + return this; + } + + public IConfigBuilder toUpperCase() { + this.toUpperCase = true; + return this; + } + + @Override + public IConfigBuilder beginConfig() { + return this; + } + + @Override + public IShapeBuilder endConfig() { + return this; + } + + @Override + public IBuilder rect() { + this.shape = new RectShape(); + return this; + } + + @Override + public IBuilder round() { + this.shape = new OvalShape(); + return this; + } + + @Override + public IBuilder roundRect(int radius) { + this.radius = radius; + float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius}; + this.shape = new RoundRectShape(radii, null, null); + return this; + } + + @Override + public TextDrawable buildRect(String text, int color) { + rect(); + return build(text, color); + } + + @Override + public TextDrawable buildRoundRect(String text, int color, int radius) { + roundRect(radius); + return build(text, color); + } + + @Override + public TextDrawable buildRound(String text, int color) { + round(); + return build(text, color); + } + + @Override + public TextDrawable build(String text, int color) { + this.color = color; + this.text = text; + return new TextDrawable(this); + } + } + + public interface IConfigBuilder { + public IConfigBuilder width(int width); + + public IConfigBuilder height(int height); + + public IConfigBuilder textColor(int color); + + public IConfigBuilder withBorder(int thickness); + + public IConfigBuilder useFont(Typeface font); + + public IConfigBuilder fontSize(int size); + + public IConfigBuilder bold(); + + public IConfigBuilder toUpperCase(); + + public IShapeBuilder endConfig(); + } + + public static interface IBuilder { + + public TextDrawable build(String text, int color); + } + + public static interface IShapeBuilder { + + public IConfigBuilder beginConfig(); + + public IBuilder rect(); + + public IBuilder round(); + + public IBuilder roundRect(int radius); + + public TextDrawable buildRect(String text, int color); + + public TextDrawable buildRoundRect(String text, int color, int radius); + + public TextDrawable buildRound(String text, int color); + } +} \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 334a1f45be..0871842a4d 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2957,4 +2957,12 @@ App aktualisiert Dein Heim-Server ist nicht erreichbar. Falls du dich dennoch abmeldest, wird dieses Gerät nicht von deiner Geräteliste entfernt, also müsstest du dies mit einer anderen Sitzung selbst machen. Dennoch abmelden + Dennoch Unterhaltung beginnen + Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du dennoch eine Unterhaltung beginnen\? +\n +\n%s + Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du sie dennoch einladen\? +\n +\n%s + Dennoch einladen \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index f4973f4b95..3c8ec19a78 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -360,11 +360,11 @@ \nΜε την απενεργοποίηση του λογαρισμού σας δεν θα ξεχαστούν τα μηνύματά που έχετε στείλει. Εάν θα θέλατε να ξεχαστούν, τότε δηλώστε το στο κουτί πιο κάτω. \n \nΗ ορατότητα μηνυμάτων δουλεύει σαν το ηλεκτρονικό ταχυδρομείο. Το να ξεχαστούν τα μηνύματα σημαίνει οτι μηνύματα τα οποία έχετε στείλει δεν θα σταλούν σε νέους ή μη-εγγεγραμμένους χρήστες, αλλα εγγεγραμμένοι χρήστες οι οποίοι έχουν ήδη πρόσβαση σε αυτά τα μηνύματα θα μπορούν ακόμα να τα δούν.
- Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό + Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό. Παράβλεψη Αποσύνδεση Δεν εμπιστεύομαι - Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο + Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο. %1$s & %2$s & άλλοι γράφουν… %s γράφει… %1$s & %2$s γράφουν… @@ -383,4 +383,132 @@ Άνοιγμα με Αποστολή αυτοκόλλητου %1$s δημιούργησε το δωμάτιο - + Εισήλθες + Αποχώρησες από το δωμάτιο + %1$s αποχώρισε από το δωμάτιο + Έχετε απορρίψει την πρόσκληση + Ορίσατε το όνομά σας ω; %1$s + Η πρόσκλησή σου + Δημιούργησες αυτό το δωμάτιο + %1$s δημιούργησε την συζήτηση + "Προσκάλεσες τον χρήστη %1$s" + Έχετε αποκλείσει τον χρήστη %1$s + Αλλάξατε το άβατάρ σας + Άλλαξες το θέμα σε: %1$s + Δωμάτιο/Χώρος + Σύνδεση + + %1$d επιλέχθηκε + %1$d επιλέχθηκαν + + + %d ACLs του server άλλαξε + %d ACLs του server άλλαξαν + + Πραγματοποιείται σύνδεση στην συσκευή + Δεν ταιριάζει; + Προσπάθησε ξανά + "Οι servers που περιλαμβάνουν %s επιτρέπονται." + Δημιούργησες αυτή την συζήτηση + Αποχώρισες από το δωμάτιο + Ο χρήστης %1$s αφαίρεσε τον αποκλείσμο του χρήστη %2$s + Άλλαξες το όνομα του δωματίου σε: %1$s + Έχετε ξεκινήσει μία βιντεοκλήση. + Το μελλοντικό ιστορικό του δωματίου θα είναι εμφανή στο χρήστη %1$s + Έχετε ξεκινησει μία κλήση. + Ο χρήστης %s αναβάθμισε αυτό το δωμάτιο. + Η εύρεση server που περιλαμβάνει %s είναι απεκλεισμένη. + "• Οι servers οπού περιλαμβάνουν %s είναι τώρα απεκλεισμένοι." + Επιβεβαίωση + Παρακαλώ επιβεβαιώστε ότι γνωρίζετε την προέλευση του κώδικα αυτού. Όταν συνδέετε νέες συσκευές, ενδεχομένως να δίνετε σε κάποιον άλλο πλήρης πρόσβαση στον λογαριασμό σας. + Εκαθάρριση προσωπικών δεδομένων + Αφαιρέσατε τον χρήστη %1$s + Ο χρήστης %1$s άλλαξε το όνομά του σε %2$s + %1$s άλλαξε το άβαταρ του δωματίου + Άλλαξες το άβαταρ του δωματίου + "%s απέστειλε δεδομένα ώστε να πραγματοποιηθεί η κλήση." + Απέστειλες δεδομένα ώστε να ξεκινήσει η κλήση. + Απάντησες στην κλήση. + Ο χρήστης %1$s έκανε τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %2$s + Έχετε αναβαθμίσει αυτό το δωμάτιο. + %s αναβαθμίστηκε εδώ. + Αναβαθμίστηκες εδώ. + %s όρισε τα ACLs αυτού του server σε αυτό το δωμάτιο. + Έχεις ορίσει το ACL του server για αυτό το δωμάτιο. + "• Οι servers που περιλαμβάνονται αυτή την IP είναι απεκλεισμένοι." + %s άλλαξε τα ACLs του server γι αυτό το δωμάτιο. + Άλλαξες τα ACLs του server γι αυτό το δωμάτιο. + Κωδικός Πρόσβασης + Οι διαχειριστές του server σας (%1$s), σας έχουν αποσυνδέσει από τον λογαριασμό σας %2$s (%3$s). + Η πειργραφή είναι πολύ σύντομη + Έχετε αποσυνδεθεί + Επιπρόσθετες ρυθμίσεις + Λειτουργίες Προγραμματιστή + Τρέχουσα συνεδρία + Διαγραφή δεδομένων + Άλλες συνεδρίες + Πραγματοποιείται η σύνδεση + Συμμετέχεις σε αυτό το δωμάτιο + %1$s εισήλθε + Έχετε αφαιρέσει τον αποκλεισμό από τον χρήστη %1$s + Αφαιρέσατε την πρόσκληση του χρήστη %1$s + Άλλαξες το όνομά σου από %1$s σε %2$s + Αφαίρεσες το όνομά όπου εμφανιζόταν ο λογαριασμός σου ( προηγουμένως ήταν %1$s) + .Τερμάτισες την κλήση. + Έκανες τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %1$s + Ορισμός συνδέσμου + Συνδεθείτε ξανά + Σύνδεση + Διαγρφή όλων των δεδομένων + Ρυθμίσεις + Γενικά + Γενικά + %1$s Έχει αφαιρεθεί το avatar του δωματίου + Έχεις αφαιρέσει το avatar του δωματίου + Καμία αλλαγή. + Έχεις αφαίρεσει το όνομα του δωματίου + Ο χρήστης %1$s προσκάλεσε τον χρήστη %2$s + Προσκάλεσες τον χρήστη %1$s + Διαχειριστής + Moderator + Άλλαξες το power του level %1$s. + Οι Servers που ταιριάζουν με %s έχουν αφαιρεθεί από την επιτρεπόμενη λίστα. + Οι Servers που ταιριάζουν με το IP έχουν αποκλειστικοί. + Έχεις αφαιρέσει το θέμα του δωματίου + Αποδέχτηκες την πρόσκληση για το %1$s + Προεπιλογή + Custom (%1$d) + Προσαρμοσμένο + 🎉Όλοι οι Servers έχουν αποκλειστεί από συμμετέχοντες!Το δωμάτιο αυτό δεν μπορεί πια να χρησιμοποιηθεί. + Έστειλες μια πρόσκληση στο %1$s για να συνδεθείς στο δωμάτιο + Το μήνυμα στάλθηκε + Δημιουργώ ένα δωμάτιο + Δεν επιτρέπεται να συνδεθείς σε αυτό το δωμάτιο + %1$s,%2$s και %3$s + Αλλάζω το Space + %1$s, %2$s, %3$s και %4$s + Ερευνάω τα δωμάτια + -Κάποιοι χρήστες έχουν αγνοηθεί + + %1$s, %2$s, %3$s και %4$d άλλος χρήστης + %1$s, %2$s, %3$s και %4$d άλλοι χρήστες + + Το μήνυμα στέλνεται… + Μύνημα + Δημιουργώ έναν σύνδεσμο + Σύνδεσμος + Στέλνω ένα αρχείο. + Μύνημα από τον χρήστη %s + Στέλνω ένα ηχητικό μύνημα. + Στέλνω μια εικόνα. + + Πρόσθεσες εναλλακτική διεύθυνση %1$ για αυτό το δωμάτιο. + Πρόσθεσες εναλλακτικές διευθύνσεις %1$ για αυτό το δωμάτιο. + + Στέλνω ένα βίντεο. + Στέλνω ένα αρχείο ήχου. + Επεξεργάζομαι ένα σύνδεσμο + Κείμενο + Στέλνω ένα αυτοκόλλητο. + Σκανάρω ένα QR code + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 81a7a10dcc..4d7cb09b96 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -1,8 +1,8 @@ - + Uzaicinājums no %s %1$s uzaicināja %2$s - %1$s uzaicināja jūs + %1$s uzaicināja Tevi %1$s pievienojās %1$s pameta istabu %1$s noraidīja uzaicinājumu @@ -39,48 +39,48 @@ Uzaicinājums uz istabu %1$s un %2$s Tukša istaba - Jūs nomainījāt savu parādāmo vārdu no %1$s uz %2$s - Jūs nomainījāt savu parādāmo vārdu uz %1$s - Jūs nomainījāt savu avataru - Jūs atsaucāt %1$s uzaicinājumu - Jūs liedzāt pieeju %1$s - Jūs atcēlāt pieejas liegumu %1$s - Jūs padzināt %1$s - Jūs noraidījāt uzaicinājumu - Jūs pametāt istabu + Tu nomainīji savu parādāmo vārdu no %1$s uz %2$s + Tu nomainīji savu parādāmo vārdu uz %1$s + Tu nomainīji savu iemiesojumu + Tu atsauci %1$s uzaicinājumu + Tu liedzi pieeju %1$s + Tu atcēli pieejas liegumu %1$s + Tu noņēmi %1$s + Tu noraidīji uzaicinājumu + Tu pameti istabu %1$s pameta istabu - Jūs pametāt istabu - Jūs pievienojāties + Tu pameti istabu + Tu pievienojies %1$s pievienojās istabai - Jūs pievienojāties istabai - Jūs uzaicinājāt %1$s - Jūs izveidojāt diskusiju + Tu pievienojies istabai + Tu uzaicināji %1$s + Tu izveidoji apspriedi %1$s izveidoja diskusiju - Jūs izveidojāt istabu + Tu izveidoji istabu %1$s izveidoja istabu - Jūsu uzaicinājums - Jūs ieslēdzāt pilnīgu šifrēšanu (neatpazīts algoritms %1$s). + Tavs uzaicinājums + Ir ieslēgta pilnīga šifrēšana (neatpazīts algoritms %1$s). %1$s ieslēdza pilnīgu šifrēšanu (neatpazīts algoritms %2$s). - Jūs ieslēdzāt pilnīgu šifrēšanu. + Tu ieslēdzi pilnīgu šifrēšanu. %1$s ieslēdza pilnīgu šifrēšanu. - Jūs esat novērsis iespēju viesiem pievienoties istabai. + Tu esi novērsis iespēju viesiem pievienoties istabai. %1$s ir novērsis iespēju viesiem pievienoties istabai. - Jūs esat novērsis iespēju viesiem pievienoties istabai. + Tu esi novērsis iespēju viesiem pievienoties istabai. %1$s ir novērsis iespēju viesiem pievienoties istabai. - Jūs esat atļāvis viesiem pievienoties istabai. + Tu esi atļāvis viesiem pievienoties šeit. %1$s ir atļāvis viesiem pievienoties istabai. - Jūs esat atļāvis viesiem pievienoties istabai. + Tu esi atļāvis viesiem pievienoties istabai. %1$s ir atļāvis viesiem pievienoties istabai. - Jūs nomainījāt adreses šai istabai. + Tu nomainīji šīs istabas adreses. %1$s nomainīja adreses šai istabai. - Jūs nomainījāt galveno un alternatīvās adreses šai istabai. + Tu nomainīji šīs istabas galveno un izvēles adreses. %1$s nomainīja galveno un alternatīvās adreses šai istabai. - Jūs nomainījāt alternatīvās adreses šai istabai. + Tu nomainīji šīs istabas izvēles adreses. %1$s nomainīja alternatīvās adreses šai istabai. - Jūs izdzēsāt šīs istabas alternatīvo adresi %1$s. - Jūs izdzēsāt šīs istabas alternatīvās adreses %1$s. - Jūs izdzēsāt šīs istabas alternatīvās adreses %1$s. + Tu noņēmi šīs istabas izvēles adresi %1$s. + Tu noņēmi šīs istabas izvēles adresi %1$s. + Tu noņēmi šīs istabas izvēles adreses %1$s. %1$s izdzēsa šīs istabas alternatīvo adresi %2$s. @@ -88,25 +88,25 @@ %1$s izdzēsa šīs istabas alternatīvās adreses %2$s. - Jūs pievienojāt šīs istabas alternatīvo adresi %1$s. - Jūs pievienojāt šīs istabas alternatīvās adreses %1$s. - Jūs pievienojāt šīs istabas alternatīvās adreses %1$s. + Tu pievienoji šīs istabas izvēles adresi %1$s. + Tu pievienoji šīs istabas izvēles adresi %1$s. + Tu pievienoji šīs istabas izvēles adreses %1$s. %1$s pievienoja šīs istabas alternatīvo adresi %2$s. %1$s pievienoja šīs istabas alternatīvās adreses %2$s. %1$s pievienoja šīs istabas alternatīvās adreses %2$s. - Jūs izdzēsāt šis istabas galveno adresi. + Tu noņēmi šīs istabas galveno adresi. %1$s izdzēsa šis istabas galveno adresi. - Jūs iestatījāt %1$s kā šis istabas galveno adresi. + Tu iestatīji %1$s kā šis istabas galveno adresi. %1$s iestatīja %2$s kā šis istabas galveno adresi. - Jūs pievienojāt %1$s un izdzēsāt %2$s kā šīs istabas adreses. + Tu pievienoji %1$s un noņēmi %2$s kā šīs istabas adreses. %1$s pievienoja %2$s un izdzēsa %3$s kā šīs istabas adreses. - Jūs izdzēsāt %1$s kā šīs istabas adresi. - Jūs izdzēsāt %1$s kā šīs istabas adreses. - Jūs izdzēsāt %1$s kā šīs istabas adreses. + Tu noņēmi %1$s kā šīs istabas adresi. + Tu noņēmi %1$s kā šīs istabas adresi. + Tu noņēmi %1$s kā šīs istabas adreses. %1$s izdzēsa %2$s kā šīs istabas adresi. @@ -114,52 +114,52 @@ %1$s izdzēsa %2$s kā šīs istabas adreses. - Jūs pievienojāt %1$s kā šīs istabas adresi. - Jūs pievienojāt %1$s kā šīs istabas adreses. - Jūs pievienojāt %1$s kā šīs istabas adreses. + Tu pievienoji %1$s kā šīs istabas adresi. + Tu pievienoji %1$s kā šīs istabas adresi. + Tu pievienoji %1$s kā šīs istabas adreses. %1$s pievienoja %2$s kā šīs istabas adresi. %1$s pievienoja %2$s kā šis istabas adreses. %1$s pievienoja %2$s kā šīs istabas adreses. - Jūs atsaucāt %1$s uzaicinājumu. Iemesls: %2$s + Tu atsauci %1$s uzaicinājumu. Iemesls: %2$s %1$s atsauca uzaicinājumu %2$s. Iemesls: %3$s - Jūs pieņēmāt uzaicinājumu %1$s. Iemesls: %2$s + Tu pieņēmi uzaicinājumu %1$s. Iemesls: %2$s %1$s pieņēma uzaicinājumu %2$s. Iemesls: %3$s - Jūs liedzāt pieeju %1$s. Iemesls: %2$s + Tu liedzi pieeju %1$s. Iemesls: %2$s %1$s liedza pieeju %2$s. Iemesls: %3$s - Jūs atcēlāt pieejas liegumu %1$s. Iemesls: %2$s + Tu atcēli pieejas liegumu %1$s. Iemesls: %2$s %1$s atcēla %2$s pieejas liegumu. Iemesls: %3$s - Jūs padzināt %1$s. Iemesls: %2$s + Tu noņēmi %1$s. Iemesls: %2$s %1$s padzina %2$s. Iemesls: %3$s - Jūs noraidījāt uzaicinājumu. Iemesls: %1$s + Tu noraidīji uzaicinājumu. Iemesls: %1$s %1$s noraidīja uzaicinājumu. Iemesls: %2$s - Jūs izgājāt. Iemesls: %1$s + Tu pameti. Iemesls: %1$s %1$s izgāja. Iemels: %2$s - Jūs pametāt istabu. Iemesls: %1$s + Tu pameti istabu. Iemesls: %1$s %1$s pameta istabu. Iemesls: %2$s - Jūs pievienojāties. Iemesls: %1$s + Tu pievienojies. Iemesls: %1$s %1$s pievienojās. Iemesls: %2$s - Jūs pievienojāties istabai. Iemesls: %1$s + Tu pievienojies istabai. Iemesls: %1$s %1$s pievienojās istabai. Iemesls: %2$s - %1$s uzaicināja jūs. Iemesls: %2$s - Jūs uzaicinājāt %1$s. Iemesls: %2$s + %1$s uzaicināja Tevi. Iemesls: %2$s + Tu uzaicināji %1$s. Iemesls: %2$s %1$s uzaicināja %2$s. Iemesls: %3$s - Jūsu uzaicinājums. Iemesls: %1$s + Tavs uzaicinājums. Iemesls: %1$s %1$s uzaicinājums. Iemesls: %2$s Sūta ziņu… Sākotnējā sinhronizācija: \nImportē konta datus Sākotnējā sinhronizācija: -\nImportē pamestās istabas +\nIegūst pamestās istabas Sākotnējā sinhronizācija: -\nImportē istabas, uz kurām uzaicināts +\nIegūst istabas, uz kurām uzaicināts Sākotnējā sinhronizācija: -\nLādē jūsu sarunas -\nJa esat pievienojies daudzām istabām, tas var aizņemt kādu laiku. +\nIelādē sarunas +\nJa ir dalība daudzās istabās, tas var aizņemt kādu laiku Sākotnējā sinhronizācija: -\nImportē istabas +\nIegūst istabas Sākotnējā sinhronizācija: \nImportē kriptogrāfiju Sākotnējā sinhronizācija: @@ -174,38 +174,38 @@ %1$s, %2$s un %3$s %1$s no %2$s uz %3$s %1$s nomainīja %2$s pieejas līmeni. - Jūs nomainījāt %1$s pieejas līmeni. + Tu nomainīji %1$s pieejas līmeni. Pielāgots Pielāgots (%1$d) Noklusējuma Moderators Administrators - Jūs pieņēmāt uzaicinājumu %1$s - Jūs atsaucāt uzaicinājumu %1$s + Tu apstiprināji uzaicinājumu %1$s + Tu atsauci uzaicinājumu %1$s %1$s atsauca uzaicinājumu %2$s - Jūs atsaucāt uzaicinājumu %1$s pievienoties istabai + Tu atsauci uzaicinājumu %1$s pievienoties istabai %1$s atsauca uzaicinājumu %2$s pievienoties istabai - Jūs uzaicinājāt %1$s + Tu uzaicināji %1$s %1$s uzaicināja %2$s - Jūs nosūtījāt %1$s uzaicinājumu pievienoties istabai - Jūs izdzēsāt istabas avataru + Tu nosūtīji %1$s uzaicinājumu pievienoties istabai + Tu izdzēsi istabas iemiesojumu %1$s izdzēsa istabas avataru - Jūs izdzēsāt istabas tematu - Jūs dzēsāt istabas nosaukumu - Jūs padarījāt turpmākās ziņas redzamas %1$s + Tu noņēmi istabas tematu + Tu noņēmi istabas nosaukumu + Tu padarīji turpmākās ziņas redzamas %1$s %1$s padarīja turpmākās ziņas redzamas %2$s - Jūs padarījāt istabas turpmāko ziņu vēsturi redzamu %1$s - Jūs beidzāt zvanu. - Jūs atbildējāt uz zvanu. - Jūs nosūtījāt datus zvana uzsākšanai. + Tu padarīji istabas turpmāko ziņu vēsturi redzamu %1$s + Tu beidzi zvanu. + Tu atbildēji uz zvanu. + Tu nosūtīji datus zvana uzsākšanai. %s nosūtīja datus zvana uzsākšanai. - Jūs veicāt balss zvanu. - Jūs veicāt video zvanu. - Jūs nomainījāt istabas nosaukumu uz %1$s - Jūs nomainījāt istabas avataru + Tu veici balss zvanu. + Tu veici video zvanu. + Tu nomainīji istabas nosaukumu uz %1$s + Tu nomainīji istabas iemiesojumu %1$s nomainīja istabas avataru - Jūs nomainījāt tematu uz %1$s - Jūs dzēsāt savu parādāmo vārdu (iepriekš %1$s) + Tu nomainīji tematu uz %1$s + Tu noņēmi savu parādāmo vārdu (tas bija %1$s) Iestatījumi Labi Atcelt @@ -218,7 +218,7 @@ Iekšējā saite Skatīt pirmkodu Skatīt atšifrētu pirmkodu - Dzēst + Izdzēst Pārdēvēt Ziņot par saturu vai @@ -250,7 +250,7 @@ Lūdzu apraksti kļūdu. Kāda darbība tika veikta? Kāds bija gaidāmais rezultāts? Kas tieši notika? Aprakstiet savu problēmu šeit Lai diagnosticētu problēmu, logfaili no šīs lietotnes tiks nosūtīti kopā ar šo kļūdas paziņojumu. Ja vēlies nosūtīt vienīgi augstākminēto tekstu, lūdzu noņem: - Jūs, šķiet, satricinājāt tālruni. Vai vēlaties iesniegt kļūdu ziņojumu? + Šķiet, ka tālrunis tiek kratīts neapmierinātības dēļ. Vai atvērt kļūdu ziņojumu skatu\? Šī programma iepriekš \"salūza\". Vai vēlies iesniegt paziņojumu par kļūdu? Paziņojums par kļūdu tika veiksmīgi nosūtīts Paziņojumu par kļūdu neizdevās nosūtīt (%s) @@ -258,7 +258,7 @@ Pievienoties istabai Lietotājvārds Izrakstīties - Mājasservera URL adrese + Mājasservera URL Meklēt Sākt audio zvanu Sākt video zvanu @@ -282,13 +282,13 @@ Šķiet ievadīta nederīga epasta adrese Šī epasta adrese jau tiek izmantota. Aizmirsāt paroli\? - Mājasservers vēlas pārbaudīt, vai neesat robots + Mājasserveris vēlas pārliecināties, ka neesi robots Neizdevās verificēt epasta adresi: pārbaudiet, vai esi noklikšķinājis(usi) uz saiti atsūtītajā epastā Ievadi korektu URL adresi Bojāts JSON Nav derīgs JSON Nosūtīti par daudz pieprasījumi - Oriģinālu + Sākotnējais Lielu Vidēju Mazu @@ -312,16 +312,16 @@ Noraidīt Pāriet uz pirmo neizlasīto ziņu Pamest istabu - Vai tiešām vēlies pamest istabu\? - TIEŠIE ČATI + Vai tiešām pamest istabu\? + Tiešās ziņas Uzaicināt Nobanot Atbanot Ignorēt Atcelt ignorēšanu Pieminēt - Jūs nevarēsiet atsaukt šīs izmaiņas, jo lietotājam būtu tāds pats piekļuves līmenis kā jums pašam. -\nVai esat pārliecināts\? + Tu nevarēsi atdarīt šo izmaiņu, jo Tu paaugstini lietotāju līdz tādam pašam spēka līmenim kā Tev pašam. +\nEsi pārliecināts\? Liedzot pieeju lietotājam, šī darbība izmetīs viņu no šīs istabas un neļaus pievienoties atkārtoti. %s raksta… %1$s & %2$s raksta… @@ -342,7 +342,7 @@ Istabas dalībnieku filtrs Rezultātu nav Visas ziņas - Pievienot uz galvenā ekrāna + Pievienot sākumekrānam Profila attēls Attēlojamais vārds Pievienot epasta adresi @@ -354,8 +354,8 @@ Iespējot šai ierīcei paziņojumus Ziņas, kuras satur manu rādāmo vārdu Ziņas, kuras satur manu lietotājvārdu - Ziņas tiešajos (privātajos) čatos - Ziņas grupu čatos + Ziņas tiešajās tērzēšanās + Ziņas grupu tērzēšanās Uzaicinājumi uz istabām Uzaicinājumi ar zvanu Ziņas no bota @@ -383,8 +383,8 @@ Piekļuve kontaktiem Kontaktu valsts Galvenais ekrāns - Piestiprināt istabas ar garām palaistiem paziņojumiem - Piestiprināt istabas ar neizlasītām ziņām + Piespraust istabas ar izlaistiem paziņojumiem + Piespraust istabas ar neizlasītām ziņām Iespējot URL priekšskatu pēc noklusējuma Vienmēr rādīt ziņu laiku Rādīt ziņu laiku 12 stundu formātā (piem. 12:12pm) @@ -410,9 +410,7 @@ Jaunā parole Paroles nomaiņas kļūda Parole nomainīta - Rādīt visas ziņas no %s\? -\n -\nŅemiet vērā, ka ar šo darbību tiks pārstartēta lietotne, un tas var aizņemt kādu laiku. + Rādīt visas ziņas no %s\? Izvēlies valsti 3 dienas 1 nedēļa @@ -422,7 +420,7 @@ Piekļuve istabas vēsturei Kas var lasīt vēsturi? Jebkurš - Tikai dalībnieki (no šī parametra iespējošanas brīža) + Tikai dalībnieki (no šīs iespējas izvēlēšanās brīža) Tikai dalībnieki (kopš tie tika uzaicināti) Tikai dalībnieki (kopš tie pievienojušies) Lietotāji, kuriem liegta pieeja @@ -474,15 +472,15 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izvēlies istabu katalogu Mājasservera nosaukums - Visas istabas %s serverī + Visas servera %s istabas Visas vietējās %s istabas - %d nelasītu paziņotu ziņu - %d nelasīta paziņota ziņa - %d nelasītas paziņotas ziņu + %d paziņojumu par nelasītām ziņām + %d paziņojums par nelasītām ziņām + %d paziņojumi par nelasītām ziņām - %d istaba + %d istabu %d istabas %d istabu @@ -495,7 +493,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lielāks Lielākais Milzīgs - Vai tiešām vēlies dzēst vidžetu? + Vai tiešām izdzēst logrīku šajā istabā\? %d aktīvs vidžets %d aktīvi vidžeti @@ -523,15 +521,15 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Sākums Istabas Uzaicināts - %2$s padzina jūs no %1$s + %2$s noņēma Tevi no %1$s %2$s liedza jums pieeju %1$s Iemesls: %1$s Avatars Kontaktu grāmata Temats Istabas nosaukums - Slepenā frāze - Iestatiet slepeno frāzi + Drošības vārdkopa + Iestatīt drošības vārdkopu Saglabājiet savu drošības atslēgu Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem, dublējot šifrēšanas atslēgas savā serverī. Iesniegt @@ -542,14 +540,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Nešifrēts vai kādu citu Matrix lietotni ar cross-signing atbalstu Šis konts ir deaktivizēts. - Šifrētas ziņas grupas čatos - Šifrētas ziņas viens-pret-vienu čatos + Šifrētas ziņas grupas tērzēšanās + Šifrētas ziņas tiešajās tērzēšanās Ziņas, kuras satur @room Šifrēšana nav iespējota - Ziņas šajā istabā ir nodrošinātas ar pilnīgu šifrēšanu. + Ziņas šajā tērzēšanā ir nodrošinātas ar pilnīgu šifrēšanu. Šifrēšana iespējota - Jauna pierakstīšanās. Vai tas bijāt jūs\? - Vai tiešām vēlaties dzēst šo notikumu\? Ņemiet vērā, ka istabas nosaukuma vai temata maiņa var atcelt izmaiņas. + Jauna pieteikšanās. Tas biji Tu\? + Vai tiešām noņemt (izdzēst) šo notikumu\? Jāņem vērā, ka, ja tika izdzēsts istabas nosaukums vai mainīts temats, tas var atdarīt izmaiņas. Apstipriniet dzēšanu @@ -562,14 +560,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verificēta Apstiprināt Verificējiet šo sesiju - Jūsu servera administrators privātajās telpās un tiešajās ziņās pēc noklusējuma ir atspējojis pilnīgu šifrēšanu. - Jaunā sesija ir verificēta un ir dota piekļuve jūsu šifrētajām ziņām, kā arī citi lietotāji redzēs, ka šī sesija ir uzticama. + Servera pārvaldītājs uzstādījis pilnīgas šifrēšanas atspējošanu privātās istabās un tiešajās ziņās kā noklusējumu. + Jaunā sesija tagad ir apliecināta. Tai ir piekļuve šifrētajām ziņām, un citi lietotāji redzēs to kā uzticamu. Saziņa ar šo lietotāju ir pilnībā šifrēta un trešās puses to nevar nolasīt. Iespējot šifrēšanu\? Iespējot pilnīgu šifrēšanu… Pārstāt ignorēt Pāriet uz pēdējo skatīto ziņu - Uzsākt dialogu + Tiešā ziņa Lietotāji Uzaicinājumi Pamet istabu… @@ -585,20 +583,20 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Drošība Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nZiņas tiek nodrošinātas ar slēdzenēm, un tikai sūtītājam un saņēmējam ir neatkārtojamas atslēgas, lai tās atslēgtu. Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nZiņas tiek nodrošinātas ar slēdzenēm, un tikai sūtītājam un saņēmējam ir neatkārtojamas atslēgas, lai tās atslēgtu. Ziņas šeit nav nodrošinātas ar pilnīgu šifrēšanu. Ziņām šajā istabā netiek piemērota pilnīga šifrēšana. - Jūs akceptējāt + Tu apstiprināji Fails Audio Attēls. Video. Lūdzu, ievadiet istabas adresi Šī adrese jau tiek izmantota - Jūs varat iespējot šo situācijā, kad istaba paredzēta izmantošanai tikai saziņai starp jūsu bāzes serverī esošajām komandām. Tas nav maināms vēlāk. + Šo varētu iespējot, ja istaba tiks izmantota tikai sadarbībai ar mājasservera iekšējām komandām. Vēlāk to nevar mainīt. Slēpt papildu iestatījumus Rādīt papildu iestatījumus Šifrēšana nevar tikt atspējota, ja reiz tikusi iespējota. @@ -621,7 +619,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Tālāk Nosūtīt atkārtoti Ievadīt kodu - Mēs tikko nosūtījām kodu uz %1$s. Ievadiet to zemāk, lai apstiprinātu, ka tas esat jūs. + Mēs tikko nosūtījām kodu uz %1$s. Tas ir jāievada zemāk, lai apstiprinātu, ka tas esi Tu. Apstipriniet tālruņa numuru Tālāk Tālruņa numurs (izvēles) @@ -629,16 +627,16 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Epasts Brīdinājums Atgriezties uz pierakstīšanos - Jūs esat izrakstījies no visām sesijām un vairs nesaņemsit push paziņojumus. Lai atkārtoti iespējotu paziņojumus, vēlreiz pierakstieties katrā ierīcē. - Jūsu parole ir atiestatīta. + Tu esi izrakstījies no visām sesijām un vairs nesaņemsi pašpiegādes paziņojumus. Lai atkārtoti iespējotu paziņojumus, vēlreiz jāpiesakās katrā ierīcē. + Parole tika atiestatīta. Esmu verificējis(usi) savu epasta adresi Turpināt - Mainot paroli, tiks atiestatītas visas pilnīgas šifrēšanas atslēgas visās jūsu sesijās, padarot šifrētās sarakstes vēsturi neizlasāmu. Pirms paroles atiestatīšanas iestatiet atslēgu dublēšanu vai eksportējiet istabas atslēgas no citas sesijas. + Paroles mainīšana atiestatīs visas pilnīgas šifrēšanas atslēgas visās sesijās, padarot šifrēto tērzēšanu vēsturi nelasāmu. Pirms paroles atiestatīšanas jāuzstāda atslēgu dublēšana vai jāizgūst istabu atslēgas no citas sesijas. Uzmanību! Jauna parole Epasts Tālāk - Apstiprinājuma vēstule tiks nosūtīta uz jūsu epasta adresi, lai apstiprinātu paroles nomaiņu. + Apliecinājuma e-pasta ziņojums tiks nosūtīts uz Tavu iesūtni, lai apstiprinātu paroles nomaiņu. Pierakstīties Reģistrēties Turpināt @@ -679,7 +677,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Istabas Sarunas Atbildēt - Rediģēt + Labot Sapratu Verificēts! Algoritms @@ -692,9 +690,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. izvērst Deaktivizēt kontu Deaktivizēt kontu - Nomaina jūsu parādāmo vārdu + Nomaina Tavu attēlojamo segvārdu Padzen lietotāju ar norādīto id - Atstāj istabu + Pamest istabu Uzaicina lietotāju ar norādīto id uz pašreizējo istabu Atceļ operatora statusu lietotājam ar norādīto Id Definē lietotāja statusu @@ -702,13 +700,13 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Parāda darbību Nerādīt nevienu šī dalībnieka ziņu Dalīties - Jūsu parādāmais vārds + Tavs attēlojamais vārds Istaba Šai istabai nav lokālās adreses Lokālās adreses Jauna publiska adrese (piemēram, #alias:server) Citas publiskotās adreses: - Publiskotas adreses ikviens var izmantot jebkurā serverī, lai pievienotos jūsu istabai. Lai varētu publiskot adresi, tai vispirms jābūt iestatītai kā lokālajai adresei. + Publiskotas adreses ikviens var izmantot jebkurā serverī, lai pievienotos Tavai istabai. Lai publiskotu adresi, tai vispirms jābūt iestatītai kā vietējai adresei. Publiskotās adreses Izmaiņas attiecībā uz to, kas var lasīt vēsturi, attieksies tikai uz nākamajiem ziņojumiem šajā telpā. Esošās vēstures redzamība nemainīsies. Parole @@ -724,9 +722,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Iespējot Dzēst %s\? Tālruņa numuri - Jūsu kontam nav pievienota neviena epasta adrese + Kontam nav pievienota neviena e-pasta adrese Epasta adreses - Jūsu kontam nav pievienots neviens tālruņa numurs + Kontam nav pievienots neviens tālruņa numurs Mainīt tematu Mainīt atļaujas Nomainīt istabas nosaukumu @@ -748,10 +746,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atcelt uzaicinājumu Atcelt lietotāja ignorēšanu Ignorēt lietotāju - Jūs nevarēsiet atcelt šīs izmaiņas pēc sava statusa pazemināšanas. Gadījumā, ja esat pēdējais priviliģētais lietotājs istabā, būs neiespējami atgūt šīs privilēģijas. - Padzīt + Tu nevarēsi atdarīt šo izmaiņu, jo Tu pazemini sevi. Ja esi pēdējais istabas lietotājs ar paaugstinātām tiesībām, būs neiespējami tās atgūt. + Noņemt no tērzēšanas Atcelt uzaicinājumu - Šī istaba nav publiska un jūs nevarēsiet atkārtoti pievienoties bez uzaicinājuma. + Šī istaba nav publiska. Tai nebūs iespējams atkārtoti pievienoties bez uzaicinājuma. Atļaujiet piekļuvi savām kontaktpersonām. Lai skenētu QR kodu, jums jāatļauj piekļuve kamerai. Notiek video zvans… @@ -804,8 +802,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %d sekundes Šī sesija ir uzticama drošai ziņojumapmaiņai, jo %1$s (%2$s) to verificēja: - Verificējiet šo sesiju, lai atzīmētu to kā uzticamu un piešķirtu piekļuvi šifrētām ziņām. Ja neesat pierakstījies šajā sesijā, jūsu konts var būt kompromitēts: - Šī sesija ir uzticama drošai ziņojumapmaiņai, jo jūs to verificējāt: + Jāapliecina šī sesija, lai atzīmētu to kā uzticamu un piešķirtu piekļuvi šifrētām ziņām. Ja netika veikta pieteikšanās šajā sesijā, konts varētu būt iesaistīts drošības pārkāpumā: + Šī sesija ir uzticama drošai ziņapmaiņai, jo Tu to apliecināji: Izrakstīties no šīs sesijas Pārvaldīt sesijas Parādīt visas sesijas @@ -817,12 +815,12 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verifikācijas slēdziens Aptauja Uzlīme - Kāds no uzskaitītajiem var būt kompromitēts: + Kaut kam no uzskaitītā var būt ietekmēta drošība: \n -\n - jūsu bāzes serveris -\n - bāzes serveris, kuram pieslēdzies verificējamais lietotājs -\n - jūsu vai otra lietotāja interneta pieslēgums -\n - jūsu vai otra lietotāja ierīce +\n - mājasserveris +\n - majassserveris, kuram pieslēdzies apliecināmais lietotājs +\n - Tavs vai otra lietotāja interneta pieslēgums +\n - Tava vai otra lietotāja ierīce Nav droša Tie nesakrīt Tie sakrīt @@ -833,18 +831,18 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Citas sesijas Pašreizējā sesija Papildu iestatījumi - Jūsu matrix.to saite ir nepareizi veidota + matrix.to saite ir nepareizi veidota Dzēst datus Dzēst visus datus Vai Nelasītas ziņas - Jūs padarījāt šo pieejamu tikai ar ielūgumiem. + Tu padarīji šo pieejamu tikai ar ielūgumiem. %1$s padarīja šo pieejamu tikai ar ielūgumiem. - Jūs padarījāt istabu pieejamu tikai ar ielūgumiem. + Tu padarīji istabu pieejamu tikai ar ielūgumiem. %1$s padarīja istabu pieejamu tikai ar ielūgumiem. - Jūs padarījāt istabu publiski pieejamu visiem, kas zina saiti. + Tu padarīji istabu publiski pieejamu ikvienam, kas zina saiti. %1$s padarīja istabu publiski pieejamu visiem, kas zina saiti. - Jūs neignorējat nevienu lietotāju + Nav vērā neņemtu lietotāju Ignorēt lietotāju ZIŅOT Ziņot par šo saturu @@ -868,7 +866,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izveido istabu… Pievienot ar QR kodu Saite nokopēta starpliktuvē - (rediģēts) + (labots) Fails %1$s ir lejupielādēts! Sūta failu (%1$s / %2$s) Šifrē failu… @@ -876,7 +874,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Šifrē sīktēlu… Balss un video Preferences - Jūs jau skataties šo istabu! + Tu jau apskati šo istabu! Citi trešo pušu paziņojumi Matrix SDK versija Šīs istabas priekšskatījums nav pieejams. Vai vēlaties tai pievienoties\? @@ -886,11 +884,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Mainīt Jums vairs nav nelasītu ziņu Nosūtīja jums uzaicinājumu - %s vēlas verificēt jūsu sesiju + %s vēlas apliecināt Tavu sesiju Verifikācijas pieprasījums Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem Neparedzēta kļūda - Izveidot frāzveida paroli + Izveidot paroles vārdkopu %d+ %1$s: %2$s Atvainotie, notikusi kļūda @@ -899,7 +897,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Sarakste turpinās šeit Šī istaba ir aizvietota un vairs nav aktīva. Pārskatīt tagad - Lai turpinātu izmantot %1$s bāzes serveri, jums ir jāpārskata un jāpiekrīt noteikumiem un nosacījumiem. + Lai turpinātu izmantot mājasserveri %1$s, ir jāpārskata un jāpiekrīt noteikumiem un nosacījumiem. Kluss Neverificēta sesija pieprasa šifrēšanas atslēgas. \nSesijas nosaukums: %1$s @@ -922,11 +920,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %1$s un %2$s %1$s iekš %2$s un %3$s Atslēgas ir veiksmīgi eksportētas - Lūdzu, izveidojiet frāzveida paroli eksportēto atslēgu šifrēšanai. Jums būs jaievada to pašu frāzveida paroli, lai varētu importēt atslēgas. + Lūgums izveidot paroles vārdkopu izsgūstamo atslēgu šifrēšanai. Būs jaievada tā pati paroles vārdkopa, lai varētu ievietot atslēgas. Istabas versija Pievienot lokālo adresi - Iestatiet šis istabas adreses, lai lietotāji var atrast šo istabu uz jūsu bāzes servera (%1$s) - Dzēst adresi \"%1$s\"\? + Iestati šīs istabas adreses, lai lietotāji var atrast šo istabu caur Tavu mājasserveri (%1$s) + Izdzēst adresi \"%1$s\"\? Skatiet un pārvaldiet šīs istabas adreses un tās redzamību istabu katalogā. Istabas adreses Atskaņot aizvara skaņu @@ -934,7 +932,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izvēlēties Noklusējuma kompresija Lai to izdarītu, iespējojiet ‘Atļaut integrācijas’ iestatījumos. - Priekšskatīt saites sarakstē, kad jūsu bāzes serveris atbalsta šo iespēju. + Priekšskatīt saites tērzēšanā, ja mājasserveris atbalsta šo iespēju. Integrācijas Atvērt iestatījumus Atjaunināt istabu @@ -952,13 +950,13 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Padzīt lietotāju Vai tiešām vēlaties atcelt uzaicinājumu šim lietotājam\? Atceļot ši lietotāja ignorēšanu, visas lietotāja ziņas atkal būs redzamas. - Ignorējot šo lietotāju noņems viņa ziņas istabās, kuras ir jums kopīgas. + Šī lietotāja neņemšana vērā kopīgajās istabās noņems viņa ziņas. \n -\nJūs varat atcelt šo darbību jebkurā brīdī vispārīgajos iestatījumos. +\nŠo darbību var jebkurā brīdī atcelt vispārīgajos iestatījumos. Pazemināt Pazemināt sevi\? Zvani - Atkārtoti pieprasīt šifrēšanas atslēgas no citām jūsu sesijām. + Atkārtoti pieprasīt šifrēšanas atslēgas no citām savām sesijām. Šis tālruņa numurs jau ir definēts. Sūtīt uzlīmi Bezvadu austiņas @@ -967,47 +965,47 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Istabu katalogs Jaunā vērtība Pārslēgt - Jūs nevarat uzsākt zvanu ar sevi, pagaidiet, kamēr dalībnieki akceptēs uzaicinājumu - Jūs nevarat uzsākt zvanu ar sevi + Nevar uzsākt zvanu ar sevi, jāpagaida, līdz dalībnieki apstiprinās uzaicinājumu + Tu nevari uzsākt zvanu ar sevi Sūtīt uzlīmi Sakarā ar pilnīgu šifrēšanu, jums var būt nepieciešams sagaidīt ziņu no kāda, jo šifrēšanas atslēgas netika pareizi nosūtītas jums. Gaida šo ziņu, tas var aizņemt ilgāku laiku Jums nav piekļuves šai ziņai - Parādīt ierīci, ar kuru jūs šobrīd varat veikt verifikāciju - Parādīt %d ierīces, ar kurām jūs šobrīd varat veikt verifikāciju - Parādīt %d ierīces, ar kurām jūs šobrīd varat veikt verifikāciju + Jāparāda %d ierīces, ar kurām šobrīd var veikt apliecināšanu + Jāparāda %d ierīce, ar kuru šobrīd var veikt apliecināšanu + Jāparāda %d ierīces, ar kurām šobrīd var veikt apliecināšanu Jums būs jāatsāk bez vēstures, ziņām, uzticamām ierīcēm un uzticamiem lietotājiem - Ja jūs atiestatīsiet visu + Ja viss tiek atiestatīts Veiciet atiestatīšanu tikai tad, ja jums vairs nav nevienas citas ierīces, ar kuru verificēt šo ierīci. Pilna atiestatīšana - Aizmirsāt vai pazaudējāt visas atkopšanās iespējas\? Atiestatiet visu + Aizmirstas vai pazaudētas visas atkopšanas iespējas\? Viss jāatiestata Izmantojiet savu %1$s vai savu %2$s, lai turpinātu. Izmantojiet jaunāko ${app_name} citās savās ierīcēs: Izmantojiet jaunāko ${app_name} citās savās ierīcēs, ${app_name} Web, ${app_name} Desktop, ${app_name} iOS, ${app_name} Android, vai kādu citu Matrix lietotni ar cross-signing atbalstu Iestatiet jaunu konta paroli… - Šis ir sākums jūsu tiešās sarakstes vēsturei ar %s. + Šis ir sākums tiešo ziņu vēsturei ar %s. Šis ir šīs sarakstes pats sākums. Šis ir pats %s sākums. - Jūs pievienojāties. + Tu pievienojies. %s pievienojās. - Jūs izveidojāt un sakonfigurējāt istabu. + Tu izveidoji un uzstādīji istabu. %s izveidoja un sakonfigurēja istabu. Šajā istabā izmantotā šifrēšana netiek atbalstīta Ziņas šajā istabā ir aizsargātas ar pilnīgu šifrēšanu. Uzziniet vairāk un verificējiet lietotājus viņu profilā. - Verifikācija atcelta - Jūsu konts varētu būt kompromitēts + Apliecināšana atcelta + Konts varētu būt iesaistīts drošības pārkāpumā Tas nebiju es Izmantojiet šo sesiju jaunās sesijas verifikācijai, tādējādi dodot piekļuvi šifrētām ziņām. - Rediģēšanas iemesls + Labošanas iemesls Iekļaut iemeslu Dzēst… - Ja jūs nevarat piekļūt esošai sesijai - Izmantot atkopšanās frāzveida paroli vai atslēgu + Ja nevar piekļūt esošai sesijai + Izmantot atkopšanas paroles vārdkopu vai atslēgu Izmantojiet citu sesiju šis sesijas verifikācijai, tādējādi dodot piekļuvi šifrētām ziņām. Iespējot šifrēšanu - Pēc iespējošanas šifrēšana istabai nevar tikt izslēgta. Šifrētā istabā sūtītas ziņas nav redzamas serverim, tikai istabas dalībniekiem. Šifrēšanas iespējošana var neļaut pareizi strādāt daudzus botiem un tiltiem. + Tiklīdz iespējots, istabas šifrēšanu nevar atspējot. Šifrētā istabā sūtītas ziņas nav redzamas serverim, tikai istabas dalībniekiem. Šifrēšanas iespējošana var neļaut pareizi strādāt daudzām robotprogrammatūrām un tiltiem. Jums nav atļaujas, lai iespējotu šifrēšanu šajā istabā. Viena persona @@ -1018,7 +1016,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verificēts %s Verificē %s Verifikācija, izmantojot emocijzīmju salīdzināšanu - Ja jūs neatrodaties klātienē, salīdziniet emocijzīmes + Ja neesi klātienē, jāsalīdzina emocijzīmes Nevar skenēt Skenēt viņu kodu Skenējiet kodu ar otra lietotāja ierīci, lai droši verificētu viens otru @@ -1026,58 +1024,58 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Verifikācijas pieprasījums Verifikācija nosūtīta %s akceptēja - Jūs atcēlāt + Tu atcēli %s atcēla Gaida… Reaģēja ar %s Bloķēt pievienošanos šai istabai ikvienam, kas nav daļa no %s Iespējot šifrēšanu Sākotnējā sinhronizācija… - Jūs izrakstījāties + Tu esi atteicies Pierakstīties vēlreiz - Jūs izrakstījāties - Neizdevās atrast derīgu bāzes serveri. Lūdzu, pārbaudiet savu identifikatoru - Šis nav pareizs identifikators. Sagaidāmais formāts: \'@user:homeserver.org\' - Ja jūs nezināt savu paroli, dodieties atpakaļ, lai to atiestatītu. - Ja izveidojat kontu bāzes serverī, izmantojiet savu Matrix ID (paraugs: @user:domain.com) un paroli zemāk. + Tu esi atteicies + Neizdevās atrast derīgu mājasserveri. Lūgums pārbaudīt savu identifikatoru + Šis nav derīgs identifikators. Sagaidāmais pieraksts: \'@user:homeserver.org\' + Ja sava parole nav zināma, jādodas atpakaļ, lai to atiestatītu. + Ja izveido kontu mājasserverī, zemāk jāizmanto savs Matrix Id (piemēram, @user:domain.com) un parole. Pierakstīties ar Matrix ID Pierakstīties ar Matrix ID - Uz šī bāzes servera darbojas pārāk veca versija. Aiciniet sava bāzes servera administratoru veikt atjauninājumus. Jūs varat turpināt, tomēr atsevišķas iespējas var nedarboties pareizi. - Novecojis bāzes serveris + Šajā mājasserverī darbojas pārāk vecs laidiens. Jāvaicā mājasservera pārvaldītājam veikt atjauninājumus. Var turpināt, tomēr atsevišķas iespējas var nedarboties pareizi. + Novecojis mājasserveris Ievadītais kods nav pareizs. Lūdzu, pārbaudiet. Mēs tikko nosūtījām epastu uz %1$s. \nLūdzu, noklikšķiniet uz saites epastā, lai turpinātu konta izveidi. Lūdzu, pārbaudiet savu epastu Pieņemt noteikumus, lai turpinātu Lūdzu, veiciet CAPTCHA izaicinājumu - Izvēlēties pielāgotu bāzes serveri + Izvēlēties pielāgotu mājasserveri Izvēlēties Element Matrix Services Izvēlēties matrix.org - Jūsu konts vēl nav izveidots.. Vai pārtraukt reģistrēšanos\? + Konts vēl nav izveidots. Vai pārtraukt reģistrēšanos\? Lūdzu, izmantojiet starptautisko formātu. - Iestatiet tālruņa numuru,lai pēc izvēles ļaut jums zināmiem cilvēkiem atrast. + Jānorāda tālruņa numurs, lai pēc izvēles ļauti sevi atklāt zināmiem cilvēkiem. Iestatiet tālruņa numuru Tālāk - Epasts (izvēles) - Iestatiet epastu, lai atgūtu savu kontu. Vēlāk jūs varat pēc izvēles ļaut jums zināmiem cilvēkiem atrast sevi pēc epasta adreses. + E-pasta adrese (izvēles) + Jānorāda e-pasta adrese, lai atkoptu kontu. Vēlāk pēc izvēles var ļaut sevi atklāt zināmiem cilvēkiem pēc šīs adreses. Iestatīt epasta adresi - Jūsu parole vēl nav nomainīta. + Parole vēl nav nomainīta. \n -\nVai pārtraukt paroles nomaiņu\? +\nPārtraukt paroles nomaiņu\? Gatavs! Nospiediet saiti, lai apstiprinātu savu jauno paroli. Kad esat sekojis saitei, noklikšķiniet zemāk. Apstiprinājuma epasts tika nosūtīts uz %1$s. Pārbaudiet ienākošos epastus - Šis epasts nav piesaistīts nevienam kontam + Šī e-pasta adrese nav piesaistīta nevienam kontam Atiestatīt paroli uz %1$s - Šis epasts nav piesaistīts nevienam kontam. - Lietotnei neizdodas izveidot kontu uz šī bāzes servera. + Šī e-pasta adrese nav piesaistīta nevienam kontam. + Lietotnei neizdodas izveidot kontu šajā mājasserverī. \n -\nVai vēlaties reģistrēties, izmantojot tīmekļa klientu\? +\nVai reģistrēties ar tīmekļa klientu\? Atvainojiet, šis serveris nepieņem jaunus kontus. - Lietotnei neizdodas pierakstīties uz šī bāzes servera. Bāzes serveris atbalsta sekojošos pierakstīšanās veidus: %1$s. + Lietotnei neizdodas pieteikties šajā mājasserverī. Mājasserveris atbalsta šo(s) pieteikšanās veidu(s): %1$s. \n -\nVai vēlaties pierakstīties, izmantojot tīmekļa klientu\? +\nVai pieteikties tīmekļa klientā\? Radās kļūda, ielādējot lapu: %1$s (%2$d) Ievadiet servera adresi, kuru vēlaties izmantot Ievadiet Modular Element vai servera adresi, kuru vēlaties izmantot @@ -1098,15 +1096,15 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Uzzināt vairāk Premium hostings organizācijām Pievienojieties bez maksas miljoniem lietotāju lielākajā publiskajā serverī - Tāpat kā ar epastu, kontiem ir sava mājvieta, lai gan jūs varat sazināties ar jebkuru citu + Tāpat kā ar e-pastu, kontiem ir sava mājvieta, lai gan var sarunāties ar ikvienu Izvēlieties serveri Uzsākt - Paplašiniet un pielāgojiet savam ērtumam + Paplašini un pielāgo savu pieredzi! Paturiet saraksti privātu ar šifrēšanas palīdzību - Sarakstieties ar cilvēkiem pa tiešo vai grupās - Tā ir jūsu sarakste. Tā pieder jums. - Paturiet ilgāk uz istabas, lai redzētu vairāk iespēju - Jūs neveicāt nekādas izmaiņas + Tērzēt ar cilvēkiem tieši vai grupās + Tā ir Tava sarakste. Tā pieder Tev. + Ilgs piespiediens uz istabas, lai redzētu vairāk iespēju + Tu neveici nekādas izmaiņas %1$s neveica nekādas izmaiņas Istabas iestatījumi Pamest istabu @@ -1131,9 +1129,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \n%s Jebkurš varēs pievienoties istabai Izveidot jaunu istabu - Jūsu istabas parādīsies šeit. Pieskarieties + labajā apakšējā stūrī, lai atrastu esošās istabas vai izveidotu jaunu. + Šeit tiks attēlotas istabas. Jāpiesit + apakšējā labajā stūrī, lai atrastu esošas vai izveidotu kādu jaunu. Atkārtot - Jūs neizmantojat nevienu identitāšu serveri + Tu neizmanto nevienu identitāšu serveri Rezerves kopiju nevarēja atšifrēt ar šo atkopšanās atslēgu: lūdzu, pārbaudiet, vai ievadījāt pareizo atkopšanās atslēgu. Kalkulē atkopšanās atslēgu… Paroles vārdkopa ir pārāk vāja @@ -1149,8 +1147,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Pievienojas istabai ar norādīto adresi Atceļ pieejas liegumu lietotājam ar norādīto id Komandai \"%s\" nepieciešami vairāk parametri vai arī kāds no parametriem ir nepareizs. - Vai tiešām vēlaties dzēst visas nenosūtītas ziņas šajā istabā\? - Dzēst nenosūtītās ziņas + Vai tiešām izdzēst visas nenosūtītas ziņas šajā istabā\? + Izdzēst nenosūtītās ziņas Ziņas neizdevās nosūtīt Vai vēlaties atcelt ziņu nosūtīšanu\? Nosūtīta @@ -1166,18 +1164,18 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atsaukt uzaicinājumu uz %1$s\? Atsaukt uzaicinājumu Meklēt kontaktus Matrix - Ielasa jūsu kontaktus… + Iegūst Tavus kontaktus… UZZINĀT VAIRĀK SAPRATU - Esam priecīgi paziņot, ka mēs esam mainījuši nosaukumu! Jūsu lietotne ir atjaunināta un esat pierakstījies savā kontā. + Esam priecīgi paziņot, ka mēs esam mainījuši nosaukumu. Lietotne ir atjaunināta, un Tu esi pierakstījies savā kontā. Riot tagad saucas Element! Gaida šifrēšanas vēsturi - Jūs veiksmīgi nomainījāt istabas iestatījumus + Tu veiksmīgi nomainīji istabas iestatījumus Loma Iestatīt lomu Atvienoties no identitāšu servera %s\? Atvērt %s noteikumus - Dalieties ar šo kodu, lai cilvēki varētu noskenēt to, konta pievienošanai un sarakstes uzsākšanai. + Ar šo kodu ir jādalās ar citiem cilvēkiem, lai viņi varētu to nolasīt, lai pievienotu Tevi un uzsāktu tērzēt. Mans kods Dalīties ar manu kodu Skenēt QR kodu @@ -1207,27 +1205,27 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Pievienot tematu Pabeigt Ievadiet savu %s, lai turpinātu. - Verifikācija tika atcelta. Jūs varat uzsākt atkal. - Kāds no uzskaitītajiem var būt kompromitēts: + Apliecināšana tika atcelta. To var uzsākt atkārtoti. + Kaut kas no uzskaitītā var būt iesaistīts drošības pārkāpumā: \n -\n- jūsu parole -\n- jūsu bāzes serveris +\n- parole +\n- mājasserveris \n- šī vai cita ierīce -\n- interneta savienojums kādai no ierīcēm +\n- interneta savienojums kādā no izmantotajām ierīcēm \n -\nMēs iesakām jums nekavējoties nomainīt paroli un atiestatīšanas atslēgu iestatījumos. +\nMēs iesakām iestatījumos nekavējoties nomainīt paroli un atkopšanas atslēgu. Atsvaidzināt Vai vēlaties sūtīt šo pielikumu %1$s\? Konta dati Lidmašīnas režīms ir ieslēgts Savienojums ar serveri ir zaudēts Gandrīz galā! Vai %s redzams tas pats vairogs\? - Kamēr šis lietotājs nav padarījis šo sesiju uzticamu, ziņas uz un no tās ir marķētas ar brīdinājumiem. Alternatīvi, jūs varat manuāli verificēt šos sesiju. + Kamēr šis lietotājs nav padarījis šo sesiju uzticamu, ziņas uz un no tās ir iezīmētas ar brīdinājumiem. To var apliecināt arī pašrocīgi. %1$s (%2$s) pierakstījās, izmantojot jaunu sesiju: Lūgums ievadīt paroles vārdkopu Paroles vārdkopa nesakrīt Piekļuve istabai - Pārvaldiet epasta adreses un tālruņu numurus, kas saistīti ar jūsu Matrix kontu + Pārvaldīt ar Matrix kontu saistītās e-pasta adreses un tālruņa numurus Epasti un tālruņa numuri Parole nav derīga Integrācija pārvaldnieks @@ -1239,9 +1237,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \nGaida servera atbildi… Pievienojiet speciālu cilni priekš nelasītiem paziņojumiem galvenajā ekrānā. Ieslēgt pavilkšanas žestu, lai atbildētu uz ziņu - Netika atrasti rediģējumi - Rediģētas Ziņas - Rādīt pilnīgu vēsturi šifrētajās istabās + Labojumi netika atrasti + Ziņu labojumi + Rādīt pilnu vēsturi šifrētajās istabās Importējiet E2E atslēgas no faila \"%1$s\". Kļūda radās vācot atslēgu dublējumu datus Kļūda radās vācot uzticības informāciju @@ -1255,23 +1253,23 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Apskatīt Reakcijas Pievienot Reakciju Piekrist - Jūsu sarunas tiks parādītas šeit. Nospiediet + ekrāna labējā, apakšējā stūrī, lai sāktu sarunas. + Šeit tiks attēlotas tiešās saziņas sarunas. Jāpiesit + apakšējā labajā stūrī, lai uzsāktu sarunu. Viss panākts! - Jūs ielūdza %s + %s uzaicināja Nezināma Kļūda Paraksts Visas atslēgas dublētas Iestatiet Drošo Dublēšanu - Dublē jūsu atslēgas. Tas var aizņemt vairākas minūtes… + Veido atslēgu rezerves kopiju. Tas var aizņemt vairākas minūtes… Pārvaldīt Atslēgu Dublējumā Jauna šifrēto ziņu atslēga Izmantot Atslēgu Dublēšanu Nekad nezaudējiet šifrētās ziņas - Dzēst savu dublēto šifrēšanas atslēgu no servera\? Jūs vairs nevarēsiet izmantot atkopšanas atsļēgu, lai lasītu šifrēto ziņu vēsturi. - Izdzēst dublējumu + Izdzēst no servera savu šifrēšanas atslēgu rezerves kopiju\? Vairs nevarēs izmantot atkopšanas atslēgu, lai lasītu šifrēto ziņu vēsturi. + Izdzēst rezerves kopiju Pārbauda dublējuma statusu Izdzēš dublējumu… - Izdzēst Dublējumu + Izdzēst rezerves kopiju Dublējums Atgūts %s ! Atbloķēt Vēsturi Importē atslēgas… @@ -1283,23 +1281,23 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izslēgt ierobežojumus Pārbaudīt fona ierobežojumus Paziņojumam tika piesists! - Neizdevās reģistrēt FCM žetonu mājasserverī: + Neizdevās reģistrēt FCM pilnvaru mājasserverī: \n%1$s - FCM žetons veiksmīgi reģistrēts mājasserverī. + FCM pilnvara veiksmīgi reģistrēta mājasserverī. FCM žetons veiksmīgi saņemts: \n%1$s FCM žetons netika saņemts: \n%1$s Žetona Reģistrācija - ${app_name} izmanto Google Play pakalpojumus, lai nodrošinātu ziņu pakalpojumu, bet tas nešķiet pareizi konfigurēts: + ${app_name} izmanto Google Play pakalpojumus, lai piegādātu pašpiegādes ziņas, bet tas nešķiet pareizi konfigurēts: \n%1$s Jums pašlaik nav iespējotas uzlīmes. \n \nPievienojiet dažas tagad\? - Neizdevās izveidot reālā laika savienojumu. -\nLūdzu, jautājiet sava servera administratoram, lai konfigurētu PAGRIEZIENA serveri, lai zvani strādātu uzticami. - Izmantojiet Integrācijas Pārvaldnieku, lai pārvaldītu botus, tiltus, logrīkus un uzlīmes. -\nIntegrācijas Pārvaldnieks saņem konfigurācijas datus un var modificēt logrīkus, sūtīt istabu uzaicinājumus jūsu vārdā. + Neizdevās izveidot reāllaika savienojumu. +\nLūgums vaicāt mājasservera pārvaldītājam uzstādīt TURN serveri, lai zvani varētu uzticami darboties. + Izmanto iekļaušanas pārvaldnieku, lai pārvaldītu robotprogrammatūru, tiltus, logrīkus un uzlīmju pakas. +\nIekļaušanas pārvaldnieks saņem konfigurācijas datus un var pārveidot logrīkus, sūtīt istabu uzaicinājumus un iestatīt spēka līmeņus Tavā vārdā. Par ienākošajiem ziņojumiem netiks paziņots, kad lietotne darbojas fonā. Bez fona sinhronizācijas Optimizēts reālajam laikam @@ -1316,7 +1314,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atvainojiet, nav atrasta ārēja aplikācija, lai pabeigtu šo darbību. ${app_name} Zvans Neizdevās Sūtīt atslēgu dalības vēsturi - Rādīt visas istabas direktorijā, tostarp istabas ar vecuma ierobežotu saturu. + Rādīt visas istabas istabu rādītājā, tostarp istabas ar vecumam ierobežotu saturu. Rādīt istabas ar vecuma ierobežojumu 🎉 Visi serveri ir aizliegti piedalīties! Šo istabu vairs nevar izmantot. Nekas nemainīts. @@ -1326,15 +1324,15 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. • Serveri, kuri atbilst %s tagad ir atļauti. • Serveri, kuri atbilst %s tika noņemti no aizliegto/nobanoto saraksta. • Serveri, kuri atbilst %s tagad ir aizliegti. - Jūs nomainījāt servera ACL šai istabai. + Tu šai istabai nomainīji servera ACL. %s nomainīja servera ACL šai istabai. • Serveri ar vienādām IP ir aizliegti. • Serveri ar vienādām IP ir atļauti. • Serveri, kuri atbilst %s ir atļauti. • Serveri, kuri atbilst %s ir nobanoti. - Jūs iestatījāt servera ACL šai istabai. + Tu šai istabai iestatīji servera ACL. %s iestatīja servera ACL šai istabai. - Jūs atjauninājāt šeit. + Tu atjaunināji šeit. %s atjaunināti šeit. Sistēmas Brīdinājumi Nepublicēt @@ -1342,56 +1340,56 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Neizdevās noņemt logrīku Neizdevās ielādēt logrīku Ziņa aizsūtīta - Jūs modificējāt %1$s logrīku + Tu pārveidoji %1$s logrīku %1$s modificēja %2$s logrīku - Jūs noņēmāt %1$s logrīku + Tu noņēmi %1$s logrīku %1$s noņēma %2$s logrīku - Jūs pievienojāt %1$s logrīku + Tu pievienoji %1$s logrīku %1$s pievienoja %2$s logrīku - Izmantojiet savu atgūšanas frāzi, lai atbloķētu - Jūs varētu zaudēt piekļuvi savām ziņām, ja izrakstīsities vai pazaudēsiet šo ierīci. + Jāizmanto atkopšanas paroles vārdkopa, lai atslēgtu šifrēto ziņu vēsturi + Var zaudēt piekļuvi savām ziņām, ja notiks atteikšanās vai tiks pazaudēta šī ierīce. Atgūšanas Atslēga Lūdzu uztaisiet kopiju Stop Aizvietot - Rezerves kopija jau pastāv jūsu mājasserverī + Rezerves kopija jau pastāv Tavā mājasserverī Atkopšanas atslēgas tika saglabātas. Saglabāt datnē Saglabāt Atgūšanas Atslēgu Es uztaisīju kopiju Saglabājiet savu atgūšanas atslēgu kaut kur ļoti drošā vietā, piemēram, paroles pārvaldniekā (vai seifā) - Jūsu atkopšanas atslēga ir drošības tīkls - to var izmantot, lai atjaunotu piekļuvi jūsu šifrētajām ziņām, ja esat aizmirsis savu paroles frāzi. -\nSaglabājiet savu atgūšanas atslēgu kaut kur ļoti drošā vietā, piemēram, paroles pārvaldniekā (vai seifā) + Atkopšanas atslēga ir drošības tīkls - to var izmantot, lai atjaunotu piekļuvi šifrētajām ziņām, ja aizmirsta paroles vārdkopa. +\nAtkopšanas atslēga ir jāglabā kādā ļoti drošā vietā, piemēram, paroļu pārvaldniekā (vai seifā) Tiek veidota atslēgu rezerves kopija. Izdevās! (Papildu) Uzstādīt ar atkopšanas atslēgu Vai aizsargā rezerves kopiju ar atkopšanas atslēgu, saglabājot to drošā vietā. Veido rezerves kopiju - Uzstādīt paroles vārdkopu - Mēs saglabāsim šifrētu jūsu atslēgu kopiju savā serverī. Aizsargājiet savu dublējumu ar frāzi, lai tā būtu droši aizsargāta. + Iestatīt paroles vārdkopu + Mēs saglabāsim šifrētu atslēgu kopiju Tavā mājasserverī. Lai rezerves kopija būtu droša, tā ir jāaizsargā ar paroles vārdkopu. \n -\nLai nodrošinātu maksimālu drošību, tam jāatšķiras no jūsu konta paroles. - Aizsargājiet savu dublējumu ar Frāzi. +\nLai nodrošinātu vislielāko drošību, tai jāatšķiras no konta paroles. + Aizsargā savu rezerves kopiju ar paroles vārdkopu! Manuāli eksportēt atslēgas (Advancēti) Sākt izmantot Atslēgu Dublēšanu - Ziņojumi šifrētās istabās ir nodrošināti ar E2E šifrēšanu. Tikai jums un saņēmējam (-iem) ir atslēgas, lai izlasītu šos ziņojumus. + Ziņojumi šifrētās istabās ir nodrošināti ar pilnīgu šifrēšanu. Tikai sarunas dalībniekiem ir atslēgas, lai izlasītu šos ziņojumus. \n -\nDroši dublējiet atslēgas, lai izvairītos no viņu zaudēšanas. +\nAtslēgas jātur drošībā, lai izvairītos no to pazaudēšanas. Nekad nepazaudējiet šifrētās ziņas - Šis serveris ir sasniedzis savu ikmēneša aktīvo lietotāju limitu. - Šis serveris ir sasniedzis savu ikmēneša aktīvo lietotāju limitu, tāpēc daži lietotāji nevarēs pierakstīties . - Šis serveris ir pārsniedzis vienu no saviem resursu ierobežojumiem. - Šis servers ir pārsniedzis vienu no saviem resursu ierobežojumiem, tāpēc daži lietotāji nevarēs ierakstīties . - kontaktēt jūsu pakalpojuma administratoru + Šis mājasserveris ir sasniedzis savu ikmēneša aktīvo lietotāju ierobežojumu. + " Šis mājasserveris ir sasniedzis savu ikmēneša aktīvo lietotāju ierobežojumu, tādēļ <b>daži lietotāji nevarēs pieteikties</b>." + Šis mājasserveris ir pārsniedzis vienu no saviem resursu ierobežojumiem. + Šis mājasserveris ir pārsniedzis vienu no saviem resursu ierobežojumiem, tādēļ <b>daži lietotāji nevarēs pieteikties</b>. + jāsazinās ar pakalpojuma pārvaldītāju Lūdzu ievadiet paroli. Lūdzu ievadiet lietotājvārdu. Lūdzu aizmirst visas ziņas, ko esmu nosūtījis, kad mans konts ir deaktivizēts (Brīdinājums: šis nākotnes lietotājiem neļaus redzēt visu sarunu kontekstu) - Tas padarīs jūsu kontu nelietojamu. Jūs nevarēsiet ierakstīties, un neviens nevarēs atkārtoti reģistrēt to pašu lietotāja ID. Tas liks jūsu kontu atstāt visas istabas, kurās piedalās, un tas noņems jūsu konta datus no jūsu identitātes servera. Šī darbība ir neatgriezeniska . + Tas padarīs kontu neizmantojamu. Nebūs iespējams pieteikties, un neviens nevarēs atkārtoti reģistrēties ar to pašu lietotāja identifikatoru. Tas liks kontam atstāt visas istabas, kurās piedalās, un tas noņems konta datus no identitātes servera. Šī darbība ir neatgriezeniska . \n -\nDeaktivizējot savu kontu , pēc noklusējuma, neliek mums aizmirst jūsu nosūtītās ziņas . Ja jūs vēlētos, lai mēs aizmirstu jūsu ziņas, lūdzu, atzīmējiet zemāk redzamo lodziņu. +\nKonta deaktivēšana pēc noklusējuma neliek mums aizmirst nosūtītās ziņas . Ja vēlme, lai mēs aizmirstu ziņas, lūgums atzīmēt zemāk esošo lodziņu. \n -\nZiņu redzamība Matrix ir līdzīga e-pastam. Mums aizmirst jūsu ziņas nozīmē, ka ziņas, ko esat nosūtījis, netiks rādītas jauniem vai nereģistrētiem lietotājiem, bet reģistrētiem lietotājiem, kuriem jau ir piekļuve šīm ziņām, joprojām būs piekļuve to kopijai. +\nZiņu redzamība Matrix ir līdzīga e-pastam. Ziņu aizmiršana nozīmē, ka nosūtītās ziņas netiks rādītas jauniem vai nereģistrētiem lietotājiem, bet reģistrētiem lietotājiem, kuriem jau ir piekļuve šīm ziņām, joprojām būs piekļuve to kopijai. Atslēgas Dalīšanās Pieprasījums Nav aktīvu logrīku Izmantot mikrofonu @@ -1399,13 +1397,13 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Bloķēt Visu Atļaut Šis logrīks vēlas izmantos tālāk norādītos resursus: - Iziet no pašreizējās konferences un pāriet uz citu\? + Pamest pašreizējo apspriedi un pārslēgties uz otru\? Atvainojiet, konferences zvani ar Jitsi netiek atbalstīti vecās ierīces (ierīces ar Android OS zem 6.0) Istabas ID Logrīka ID - Jūsu motīvs - Jūsu lietotāja ID - Jūsu avatāra URL + Tavs izskats + Tavs lietotāja Id + Tava iemiesojuma URL Atsaukt piekļuvi man Atvērt pārlūkprogrammā Pārlādēt logrīku @@ -1427,8 +1425,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Publicēt jaunu adresi manuāli Šī ir galvenā adrese ${app_name} apkopo anonīmu analītiku, lai ļautu mums uzlabot aplikāciju. - Šis aizvietos jūsu esošo Atslēgu vai Frāzi. - Izveidot jaunu Drošības Atslēgu vai iestatīt jaunu Drošības Frāzi jūsu esošajam dublējumam. + Šis aizvietos esošo atslēgu vai vārdkopu. + Izveidot jaunu drošības atslēgu vai iestatīt jaunu drošības vārdkopu esošajai rezerves kopijai. Iestatīt uz šīs ierīces Atiestatīt Drošo Dublējumu Pievienot pogu ziņu ievades laukā priekš Emoji @@ -1439,14 +1437,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Rādīt konta notikumus Rādīt cilvēku pievienošanos vai iziešanu Izmantojiet /konfeti komandu vai nosūtiet ziņu, kas satur ❄️ or 🎉 - Rādīt sarunu efektus + Rādīt tērzēšanas iespaidus Nospiediet izlasīšanas simbolu, priekš detalizētas informācijas. Rādīt vai ziņa ir izlasīta/neizlasīta Formatēt ziņas advancēti. Tas ļauj uzlabot formatējumu, piemēram, izmantojot zvaigznītes, lai parādītu slīprakstu tekstu. Advancēts formatējums Kriptogrāfijas Atslēgu Pārvaldība - Ļaut citiem lietotājiem zināt, ka jūs rakstāt. - Jūs skatāties ziņojumā! Klikšķiniet šeit! + Ļaut citiem lietotājiem zināt, ka Tu raksti. + Tu apskati paziņojumu. Klikšķini šeit! Pievienot Kontu Firebase Tokens Salabot Play Servisus @@ -1459,16 +1457,16 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Akumulatora Optimizācija Ieslēgt Aplikācijas palaišanu kopā ar telefonu Play Servisu Pārbaude - Daži paziņojumi ir izslēgti jūsu pielāgotos iestatījumos. + Daži paziņojumi ir izslēgti pielāgotajos iestatījumos. Ievērojiet, ka daži ziņojumi ir iestatīti kā klusi (paziņojumi pienāks bez skaņas). Pielāgoti Iestatījumi. Paziņojumi nav ieslēgti šai sesijai. \nLūdzu, pārbaudiet ${app_name} iestatījumus. Paziņojumi ir ieslēgti šai sesijai. Sesijas Iestatījumi. - Paziņojumi ir atspējoti jūsu kontā. -\nLūdzu, pārbaudiet konta iestatījumus. - Paziņojumi ir ieslēgti jūsu kontam. + Konta paziņojumi ir atspējoti. +\nLūgums pārbaudīt konta iestatījumus. + Konta paziņojumi ir ieslēgti. Konta iestatījumi. Paziņojumi ir izslēgti sistēmas iestatījumos. \nLūdzu, pārbaudiet sistēmas iestatījumus. @@ -1476,13 +1474,13 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Sistēmas iestatījumi. Viens vai vairāki testi nav izdevušies, lūdzu, iesniedziet kļūdu ziņojumu, lai palīdzētu mums izmeklēt. Viens vai vairāki testi nav izdevušies, mēģiniet ieteiktos labojumus. - Pēc izskata ir labi. Ja jūs joprojām nesaņemat paziņojumus, lūdzu, iesniedziet kļūdu ziņojumu, lai palīdzētu mums izmeklēšanā. + Pēc pamata pārbaudes ir labi. Ja joprojām netiek saņemti paziņojumi, lūgums iesniegt kļūdu ziņojumu, lai palīdzētu mums izpētīt cēloni. Palaist Testus Pārbaudes diagnostika Paziņojumu pārbaude Paziņojumu svarīgums Paziņojumu papildu iestatījumi - Jūs apturējāt zvanu + Tu aizturēji zvanu %s apturēja zvanu Apturēt Turpināt @@ -1493,44 +1491,44 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Novērst nejaušu zvanu SSL kļūda: Peer identitāte nav pārbaudīta. SSL kļūda. - Šī URL serveris nav sasniedzams, lūdzu, pārbaudiet to + Ar šo URL mājasserveris nav sasniedzams, lūgums pārbaudīt to Šī nav derīga Matrix servera adrese - Lūdzu, pārskatiet un pieņemiet šī servera noteikumus: + Lūgums pārskatīt un pieņemt šī mājasservera nosacījumus: Ieslēgt HD Izslēgt HD Priekšējā Pārslēgt kameru Izvēlieties Skaņas Ierīci - Sanāksmes izmanto Jitsi drošības un atļauju politiku. Visi cilvēki, kas pašlaik atrodas telpā, redzēs aicinājumu pievienoties, kamēr notiek jūsu sanāksme. + Sanāksmes izmanto Jitsi drošības un atļauju nosacījumus. Visi cilvēki, kas pašlaik atrodas telpā, redzēs uzaicinājumu pievienoties, kamēr notiek sanāksme. Sākt audio sanāksmi Sākt video sanāksmi Jums nav atļaujas sākt zvanu Jums nav atļaujas sākt zvanu šajā istabā Jums nav atļaujas sākt konferences zvanu - Sākt sarunu - Ja jūs nedublēsiet atslēgas pirms izrakstīšanās, zaudēsiet piekļuvi jūsu šifrētām ziņām. - Drošs Atslēgu Dublējums jābūt ieslēgts visās jūsu sesijās, lai izvairītos no piekļuves zaudēšanas jūsu šifrētām ziņām. + Uzsākt tērzēšanu + Tu zaudēsi piekļuvu savām šifrētajām ziņām, ja pirms atteikšanās neveiksi atslēgu rezerves kopēšanu. + Drošai atslēgu rezerves kopēšanai jābūt ieslēgtai visās sesijās, lai izvairītos no piekļuves zaudēšanas šifrētajām ziņām. Dublēt Dublē atslēgas… Es nevēlos manas šifrētās ziņas - Notiek atslēgu dublējums. Ja jūs izrakstīsities tagad, jūs zaudēsiet piekļuvi jūsu šifrētām ziņām. - Jūs zaudēsiet savas šifrētās ziņas, ja izrakstīsities tagad + Notiek atslēgu rezerves kopēšana. Ja Tu tagad izrakstīsies, Tu zaudēsi piekļuvi savām šifrētajām ziņām. + Tu zaudēsi savas šifrētās ziņas, ja tagad atteiksies Izmantot Atslēgu Dublējumu Atslēgu Dublējums - Jūs atjauninājāt šo istabu. + Tu atjaunināji šo istabu. %s atjaunināja šo istabu. - Atjaunošanas Frāze + Atkopšanas paroles vārdkopa Atbloķēt šifrēto ziņu vēsturi ${app_name} Android Atslēgas jau ir atjauninātas! Pasākumu moderē telpas administrators, iemesls: %1$s Lietotājs izdzēsa notikumu, iemesls: %1$s - Dzēst %1$s tipa konta datus\? + Izdzēst %1$s veida konta datus\? \n -\nLietojiet piesardzīgi, tas var izraisīt neparedzētu uzvedību. +\nJāizmanto piesardzīgi, jo tas var izraisīt neparedzētu uzvedību. Izstrādātāj rīki Nav pieejama kriptogrāfiska informācija - Jūsu serveris pieņem pielikumus (failus, multivides u.c.), kuru izmērs nepārsniedz %s. + Mājasserveris pieņem pielikumus (datnes, multivides u.c.), kuru izmērs nepārsniedz %s. Robeža nav zināma. Servera failu augšupielādes ierobežojums Servera versija @@ -1543,12 +1541,12 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izstrādātāja režīms aktivizē slēptās funkcijas un var arī padarīt lietojumprogrammu mazāk stabilu. Tikai izstrādātājiem! Izstrādātāja režīms Apraksts ir pārāk īss - Jūs zaudēsiet piekļuvi šifrētām ziņām, ja nepierakstīsities, lai atgūtu šifrēšanas atslēgas. + Tiks zaudēta piekļuve šifrētām ziņām, kamēr nepieteiksies, lai atkoptu šifrēšanas atslēgas. Skatīja - Ir nosūtīts pārāk daudz pieprasījumu. Jūs varat atkārtot mēģinājumu pēc %1$d sekundēm… - Ir nosūtīts pārāk daudz pieprasījumu. Jūs varat atkārtot mēģinājumu pēc %1$d sekundes… - Ir nosūtīts pārāk daudz pieprasījumu. Jūs varat atkārtot mēģinājumu pēc %1$d sekundēm… + Ir nosūtīts pārāk daudz pieprasījumu. Pēc %1$d sekundēm var mēģināt vēlreiz… + Ir nosūtīts pārāk daudz pieprasījumu. Pēc %1$d sekundes var mēģināt vēlreiz… + Ir nosūtīts pārāk daudz pieprasījumu. Pēc %1$d sekundēm var mēģināt vēlreiz… Ievadiet atslēgvārdus, lai atrastu reakciju. Spoileris @@ -1574,30 +1572,30 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Pāriet uz apakšu Aizvērt atslēgu dublējuma baneri Izveidot jaunu telpu - Lūdzu, mēģiniet vēlreiz, tiklīdz esat piekritis sava servera noteikumiem un nosacījumiem. - Pašlaik jūs kopīgojat e-pasta adreses vai tālruņa numurus identitātes serverī %1$s. Lai pārtrauktu to koplietošanu, jums būs jāpārveido savienojums ar %2$s. - Jūsu izvēlētajam identitātes serverim nav pakalpojumu sniegšanas noteikumu. Turpiniet tikai tad, ja uzticaties pakalpojuma īpašniekam + Lūgums mēģināt atkārtoti, tiklīdz esi piekritis sava mājasservera noteikumiem un nosacījumiem. + Pašlaik e-pasta adreses vai tālruņa numuri tiek kopīgoti identitātes serverī %1$s. Atkārtoti jāsavienojas ar %2$s, lai apturētu to kopīgošanu. + Izvēlētajam identitātes serverim nav pakalpojumu sniegšanas noteikumu. Turpināt ir ieteicams tikai tad, ja ir uzticēšanās pakalpojuma īpašniekam Identitātes serverim nav pakalpojumu sniegšanas noteikumu Lūdzu, ievadiet identitātes servera url Ievadiet identitātes servera URL Sniegt piekrišanu Atsaukt manu piekrišanu - Jūs esat devis savu piekrišanu sūtīt e-pastus un tālruņa numurus uz šo identitātes serveri, lai atklātu citus lietotājus no jūsu kontaktiem. + Ir dota piekrišana sūtīt e-pasta adreses un tālruņa numurus uz šo identitātes serveri, lai atklātu citus lietotājus no kontaktiem. Sūtīt e-pastus un tālruņa numurus - Mēs nosūtījām jums apstiprinājuma e-pastu uz %s, lūdzu, vispirms pārbaudiet savu e-pastu un noklikšķiniet uz apstiprinājuma saites - Mēs nosūtījām jums apstiprinājuma e-pastu uz %s, pārbaudiet savu e-pastu un noklikšķiniet uz apstiprinājuma saites + Mēs nosūtījām apstiprinājuma e-pastu uz %s, lūgums vispirms pārbaudīt e-pasta iesūtni un noklikšķināt uz apstiprinājuma saites + Mēs nosūtījām apstiprinājuma e-pastu uz %s, jāpārbauda e-pasta iesūtne un jāklikšķina uz apstiprinājuma saites Atrodamie tālruņa numuri - Atvienošanās no identitātes servera nozīmē, ka citi lietotāji nevarēs jūs atrast, un jūs nevarēsiet uzaicināt citus lietotājus, izmantojot e-pastu vai tālruni. - Pēc tālruņa numura pievienošanas tiks parādītas atklāšanas opcijas. - Pēc e-pasta pievienošanas tiks parādītas atklāšanas opcijas. + Atvienošanās no identitātes servera nozīmē, ka nebūsi atklājams citiem un nevarēsi uzaicināt citus, izmantojot e-pasta adresi vai tālruņa numuru. + Atklāšanas iespējas parādīsies tiklīdz būs pievienots tālruņa numurs. + Atklāšanas iespējas parādīsies tiklīdz būs pievienota e-pasta adrese. Atklājamās e-pasta adreses - Pašlaik neizmantojat identitātes serveri. Lai atklātu esošos kontaktus un būtu atrodami jūsu zināmajiem kontaktiem, konfigurējiet šādu serveri. + Pašlaik netiek izmantots identitātes serveris. To var norādīt zemāk, lai atklātu esošos kontaktus un būtu atklājams tiem. Pašlaik izmantojat %1$s, lai atrastu un būtu atrodams esošajiem kontaktiem, kurus pazīstat. Mainīt identitātes serveri - Izmantojiet Botus, tiltus, logrīkus un uzlīmju paketes + Izmanto robotprogrammatūru, tiltus, logrīkus un uzlīmju pakas Esiet atrodams citiem Pakalpojumu sniegšanas noteikumi - Skatīt Rediģēšanas Vēsturi + Skatīt labošanas vēsturi Rādīt slēptos notikumus laika joslā Reģistrēt žetonu @@ -1605,14 +1603,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Dublē %d atslēgu… Dublē %d atslēgas… - Lai šajā sesijā izmantotu Atslēgu Dublēšanu, tagad atjaunojiet, izmantojot savu piekļuves frāzi vai atkopšanas atslēgu. + Lai šajā sesijā izmantotu atslēgu rezerves kopēšanu, tagad jāatjauno ar savu paroles vārdkopu vai atkopšanas atslēgu. Rezerves kopijai ir nederīgs paraksts no nepārbaudītas sesijas %s Rezerves kopijai ir nederīgs paraksts no pārbaudītas sesijas %s Rezerves kopijai ir derīgs paraksts no nepārbaudītas sesijas %s Rezerves kopijai ir derīgs paraksts no pārbaudītas sesijas %s. Rezerves kopijai ir derīgs šīs sesijas paraksts. Rezerves kopijai ir paraksts no nezināmas sesijas ar ID %s. - Jūsu atslēgas netiek dublētas no šīs sesijas. + Šīs sesijas atslēgām netiek veikta rezerves kopēšana. Šajā sesijā Atslēgu Dublēšana nav aktīva. Atslēgu Dublēšana šai sesijai ir pareizi iestatīta. Atjaunot no Dublējuma @@ -1623,7 +1621,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lūdzu, ievadiet atgūšanas atslēgu Rezerves kopijas atjaunošana: - Rezerves kopiju nevarēja atšifrēt ar šo piekļuves frāzi: lūdzu, pārbaudiet, vai ievadījāt pareizu atgūšanas piekļuves frāzi. + Rezerves kopiju nevarēja atšifrēt ar šo paroles vārdkopu: lūgums pārbaudīt, vai ir ievadīta pareiza atkopšanas paroles vārdkopa. Vai esat pazaudējis atgūšanas atslēgu\? Iestatījumos varat iestatīt jaunu. Ievadiet Atgūšanas Atslēgu Kopīgojiet atkopšanas atslēgu ar… @@ -1661,8 +1659,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Priekšskatīt failu pirms nosūtīšanas ${app_name} sinhronizēsies fonā periodiski, precīzi, noteiktā laikā (konfigurējams). \nTas ietekmēs radio un akumulatora izmantošanu, tiks parādīts pastāvīgs paziņojums par to, ka ${app_name} klausās notikumus. - Ja lietotājs kādu laiku atstāj ierīci atvienotu no tīkla un nekustīgu, ar izslēgtu ekrānu, ierīcē tiek ieslēgts dīkstāves režīms. Tas neļauj lietotnēm piekļūt tīklam un atliek to darbu izpildi, sinhronizāciju un standarta trauksmes signālus. - Pakalpojums nesāksies, kad ierīce tiks restartēta, jūs nesaņemsiet paziņojumus, kamēr vismaz vienreiz nebūsiet atvēris ${app_name}. + Ja lietotājs kādu laiku atstāj ierīci atvienotu no tīkla un nekustīgu, ar izslēgtu ekrānu, ierīcē tiek ieslēgts dīkstāves režīms. Tas neļauj lietotnēm piekļūt tīklam un atliek to darbu izpildi, sinhronizāciju un standarta brīdinājumus. + Pakalpojums nesāksies, kad ierīce tiks pārsāknēta, kamēr ${app_name} netiks atvērta vismaz vienreiz, netiks saņemti paziņojumi. Lūdzu, noklikšķiniet uz paziņojuma. Ja paziņojums nav redzams, pārbaudiet sistēmas iestatījumus. %d izvēlēti @@ -1674,7 +1672,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lai veiktu šo darbību, lūdzu, piešķiriet kamerai atļauju sistēmas iestatījumos. Lai veiktu šo darbību, trūkst dažu atļauju. Lūdzu, sistēmas Iestatījumos piešķiriet atļaujas. Vietnes - Ieteiktās telpas + Ieteiktās istabas Pievienoties tāpat Pievienoties Telpai Izveidot Telpu @@ -1698,8 +1696,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izveidosim katram no tiem savu istabu. Vēlāk varat pievienot arī citas, tostarp jau esošās. Pie kādām lietām strādājat\? Nodrošiniet piekļuvi %s uzņēmumam pareizajiem cilvēkiem. Vēlāk varat uzaicināt vairāk. - Kas ir jūsu komandas biedri\? - Mēs izveidosim tām istabas. Vēlāk varat pievienot arī citas. + Kas ir Tavi komandas biedri\? + Mēs izveidosim tām istabas. Vēlāk var pievienot arī citas. Kādas ir dažas diskusijas, ko vēlaties apspriest %s\? Dodiet tam nosaukumu, lai turpinātu. Pievienojiet sīkāku informāciju, lai palīdzētu cilvēkiem to identificēt. Tās var mainīt jebkurā brīdī. @@ -1712,14 +1710,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Privāta Telpa jums un biedriem Es un biedri Pārliecinieties, ka piekļuve %s ir pareizajiem cilvēkiem. To var mainīt vēlāk. - Ar ko jūs strādājat\? - Privāta telpa, kur sakārtot istabas + Ar ko Tu strādā\? + Privāta vieta, kur sakārtot istabas Tikai es Lai pievienotos esošai Telpai, ir nepieciešams ielūgums. To var mainīt vēlāk Kādu Telpu vēlaties izveidot\? - Jūsu privātā Telpa - Jūsu publiskā Telpa + Tava privātā vieta + Tava publiskā vieta Pievienot Telpu Privāta Telpa Publiska Telpa @@ -1736,7 +1734,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atvērt emocijzīmju izvēlni Izvēlēts Video - Izdzēst avatāru + Izdzēst iemiesojumu Mainīt avatāru Bilde Atvērt logrīkus @@ -1764,7 +1762,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Piezvanīt atpakaļ Zvans beidzās %1$s atteica zvanu - Jūs atteicāt šo zvanu + Tu atteici šo zvanu Telpa vēl nav izveidota. Atcelt telpas izveidi\? Nevar sūtīt ziņas sev! Nomainīt tagadējo PIN @@ -1781,11 +1779,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izslēgt kameru Ieslēgt mikrofonu Izslēgt mikrofonu - Varat ievadīt jebkura cita identitātes servera URL adresi. - Varat arī ievadīt jebkuru citu identitātes servera URL. + Jāievada identitātes servera URL + Var ievadīt arī jebkura cita identitātes servera URL Izmantot %1$s - Jūsu mājasserveris (%1$s) ierosina identitātes serverim izmantot %2$s - Jūsu privātuma dēļ ${app_name} atbalsta tikai šifrētu lietotāja e-pasta vēstules un tālruņa numura nosūtīšanu. + Mājasserveris (%1$s) ierosina identitātes serverim izmantot %2$s + Privātuma nolūkos ${app_name} nodrošina ar jaucējkodu slēptu lietotāja e-pasta adrešu un tālruņa numuru sūtīšanu. Vispirms iestatījumos pieņemiet identitātes servera noteikumus. Vispirms konfigurējiet identitātes serveri. Šī darbība nav iespējama. Mājasserveris ir novecojis. @@ -1807,19 +1805,19 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Nokopējiet to uz personālās mākoņa krātuves Saglabājiet uz USB vai rezerves diska Izprintējiet un turiet to drošībā - Jūsu %2$s un %1$s tagad ir iestatīti. + Tavs %2$s un %1$s tagad ir iestatīti. \n -\nSaglabājiet tos drošībā! Tie jums būs nepieciešami, lai atbloķētu šifrētus ziņojumus un aizsargātu informāciju, ja zaudēsiet visas aktīvās sesijas. +\nTie ir jātur drošībā. Tie būs nepieciešami, lai atslēgtu šifrētus ziņojumus un aizsargātu informāciju, ja tiks zaudētas visas darbojošās sesijas. Turiet to drošībā Viss pabeigts! Šis varētu aizņemt dažas sekundes. - Ievadiet drošības frāzi kuru zināt tikai jūs, lai nodrošinātu noslēpumus jūsu serverī. + Jāievada tikai sev zināma drošības vārdkopa, kas tiek izmantota, lai noslēpumus serverī padarītu drošus. Neizmantojiet sava konta paroli. Ziņu Atslēga - Jūs neverificēsiet %1$s (%2$s, ja atcelsiet tagad. Sāciet no jauna lietotāja profilā. + %1$s netiks apliecināts (%2$s), ja tagad atcelsi. Lietotāja profilā jāsāk no jauna. Ja to atcelsiet, jaunajā ierīcē nevarēsiet lasīt šifrētos ziņojumus Ja to atcelsiet, šajā ierīcē nevarēsiet lasīt šifrētus ziņojumus - Nosūtīt multividi ar oriģinālo izmēru + Nosūtīt multividi ar sākotnējo izmēru %d aktīvu sesiju %d aktīva sesiju @@ -1831,10 +1829,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Šis serveris jau ir iekļauts sarakstā Nevar atrast šo serveri vai tā istabu sarakstu Ievadiet jaunā servera nosaukumu, kuru vēlaties izpētīt. - Citas Telpas vai istabas, par kurām jūs, iespējams, nezināt + Citas vietas vai istabas, kuras varētu būt zināmas Telpa, kuru zināt, kas satur šo istabu Izlemiet, kurš var atrast šo istabu un pievienoties tai. - Pieskarieties, lai rediģētu Telpas + Piesist, lai labotu vietas Izvēlaties Telpas Izlemiet, kuras Telpas var piekļūt šai istabai. Ja Telpa ir izvēlēta, tās dalībnieki varēs atrast istabas nosaukumu un pievienoties tai. Telpas, kurām var piekļūt @@ -1853,11 +1851,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Skatiet un pārvaldiet adreses šajā telpā. Kurš var piekļūt\? Paziņojumus varat pārvaldīt sadaļā %1$s. - Lūdzu, ņemiet vērā, ka atsauču un atslēgvārdu paziņojumi nav pieejami šifrētās istabās mobilajā ierīcē. - Maina jūsu parādīto segvārdu tikai šajā telpā. + Lūgums ņemt vērā, ka šifrētu istabu pieminēšanas un atslēgvārdu paziņojumi nav pieejami viedierīcēs. + Maina attēlojamo segvārdu tikai pašreizējā istabā Iestata istabas nosaukumu Pievienot jaunu serveri - Jūsu servers + Tavs serveris Publisks Privāts Telpas adrese @@ -1865,14 +1863,14 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Telpas piekļuve Konta iestatījumi - Sūtīt video ar oriģinālo izmēru - Sūtīt video ar oriģinālo izmēru - Sūtīt video ar oriģinālo izmēru + Nosūtīt video ar sākotnējo izmēru + Nosūtīt video ar sākotnējo izmēru + Nosūtīt video ar sākotnējo izmēru - Sūtīt attēlu ar oriģinālo izmēru - Sūtīt attēlu ar oriģinālo izmēru - Sūtīt attēlus ar oriģinālo izmēru + Nosūtīt attēlus ar sākotnējo izmēru + Nosūtīt attēlu ar sākotnējo izmēru + Nosūtīt attēlus ar sākotnējo izmēru nestabila stabila @@ -1888,29 +1886,29 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Noskenēt ar šo ierīci Noskenējiet kodu ar citu ierīci vai pārslēdziet un skenējiet ar šo ierīci Balss - Jūsu e-pasta domēnu nav atļauts reģistrēt šajā serverī + Šādam e-pasta domēnam nav ļauta reģistrēšanās šajā serverī Izveido Telpu… Telpas adrese Pievieno ( ͡° ͜ʖ ͡°) parasta teksta ziņai Rādīt kādu noderīgu informāciju, lai palīdzētu atkļūdošanas programmā Parādīt atkļūdošanas informāciju ekrānā ${app_name} var biežāk avarēt, ja rodas neparedzēta kļūda - Pašreizējā sesija ir lietotājam %1$s, un jūs sniedzat lietotāja %2$s akreditācijas datus. To neatbalsta ${app_name}. -\nLūdzu, vispirms notīriet datus un pēc tam pierakstieties vēlreiz, izmantojot citu kontu. + Pašreizējā sesija ir lietotājam %1$s, un tiek sniegti lietotāja %2$s pieteikšanās dati. ${app_name} to nenodrošina. +\nLūgums vispirms notīrīt datus un pēc tam pieteikties vēlreiz citā kontā. Izdzēst visus šajā ierīcē pašlaik saglabātos datus\? \nPierakstieties vēlreiz, lai piekļūtu konta datiem un ziņojumiem. - Brīdinājums: Šajā ierīcē joprojām tiek glabāti jūsu personas dati (tostarp šifrēšanas atslēgas). + Brīdinājums: šajā ierīcē joprojām tiek glabāti personīgie dati (tostarp šifrēšanas atslēgas). \n -\nIzdzēsiet tos, ja esat beidzis lietot šo ierīci vai vēlaties pierakstīties citā kontā. +\nTie ir jānotīra, ja šī ierīce vairs netiks izmantota vai ir vēlēšanās pierakstīties citā kontā. Pierakstieties, lai atgūtu šifrēšanas atslēgas, kas glabājas tikai šajā ierīcē. Jums tās ir nepieciešamas, lai lasītu visus savus drošos ziņojumus jebkurā ierīcē. - Jūsu mājasservera (%1$s) administrators ir izslēdzis jūs no konta %2$s (%3$s). + Mājasservera (%1$s) pārvaldītājs ir izrakstījis Tevi no konta %2$s (%3$s). To var izraisīt dažādi iemesli: \n -\n• Jūs esat mainījis paroli citā sesijā. +\n• Citā sesijā ir nomainīta parole. \n -\n• Jūs esat izdzēsis šo sesiju no citas sesijas. +\n• Citā sesijā ir izdzēsta šī sesija. \n -\n• Jūsu servera administrators ir anulējis jūsu piekļuvi drošības apsvērumu dēļ. +\n• Servera pārvaldītājs piekļuvi ir padarījis par spēkā neesošu drošības apsvērumu dēļ. Neizskatās pēc derīgas e-pasta adreses Nosūta doto ziņojumu kā pārsteigumu %1$s pie %2$s @@ -1919,9 +1917,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Nevarēja apstrādāt kopīgošanas datus Fails ir pārāk liels, lai tos augšupielādētu. - %1$s, %2$s un %3$d citi lasīja - %1$s, %2$s un %3$d citi lasīja - %1$s, %2$s un %3$d citi lasīja + %1$s, %2$s un %3$d citi izlasīja + %1$s, %2$s un %3$d citi izlasīja + %1$s, %2$s un %3$d citi izlasīja Izveidot jaunu tiešo sarunu Aizveriet telpas izveides izvēlni… @@ -1938,24 +1936,24 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Kompresē bildi… Sniedziet atsauksmes Atsauksmi neizdevās nosūtīt (%s) - Paldies, jūsu atsauksme ir veiksmīgi nosūtīta + Paldies, atsauksme tika veiksmīgi nosūtīta Ja jums ir kādi papildjautājumi, varat sazināties ar mani - Jūs izmantojat Telpu beta versiju. Jūsu atsauksmes palīdzēs izstrādāt nākamās versijas. Jūsu platforma un lietotājvārds tiks atzīmēti, lai mēs varētu pēc iespējas labāk izmantot jūsu atsauksmes. + Tu izmanto vietu beta laidienu. Atsauksmes palīdzēs uzlabot nākamos laidienus. Izmantotā sistēma un lietotājvārds tiks pierakstīts, lai mēs varētu pēc iespējas labāk izmantot atsauksmes. Atsauksmes Atsauksmes par Telpām Format: Url: - session_name: - app_display_name: - push_key: - app_id: + Sesijas attēlojamais nosaukums: + Lietotnes attēlojamais nosaukums: + Iesūtīšanas atslēga: + Lietotnes identifikators: Nav reģistrētu Palaišanas vārtu Nav iestatīti Palaišanas Noteikumi Palaišanas Noteikumi Radīt jaunu Telpu - Izskatās, ka mēģināt izveidot savienojumu ar citu mājasserveri. Vai vēlaties izrakstīties\? + Izskatās, ka mēģini izveidot savienojumu ar citu mājasserveri. Vai atteikties\? Paziņot mani priekš - Jūs nesaņemsiet paziņojumus par pieminējumiem un atslēgvārdiem šifrētās telpās mobilajās ierīcēs. + Viedierīcēs netiks saņemti paziņojumi par pieminēšanu un atslēgvārdiem šifrētās istabās. Istabu atjauninājumi Ziņas no bota Istabu ielūgumi @@ -1963,7 +1961,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \@istaba Šifrētās grupu ziņas Grupu ziņas - Šifrētās tiešās ziņas + Šifrētas tiešās ziņas Tiešās ziņas Mans lietotājvārds Neizdevās iegūt jaunāko atjaunošanas atslēgu versiju (%s). @@ -1972,9 +1970,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %d jauna atslēgu tika pievienotas šai sesijai. %d jaunas atslēgu tika pievienotas šai sesijai. - Neziniet atkopšanas paroli, varat %s. + Ja nav zināma atkopšanas paroles vārdkopa, tad var %s. Iegūst rezerves kopiju… - Atjaunošanas atslēgas ģenerēšana, izmantojot paroli, šis process var aizņemt vairākas sekundes. + Izveido atkošanas atslēgu, izmantojot paroles vārdkopu, tas var aizņemt vairākas sekundes. Izskatās, ka jums jau ir izveidots atslēgas dublējums no citas sesijas. Vai vēlaties to aizstāt ar izveidoto\? Lūgums izdzēst paroles vārdkopu, ja ir vēlams, lai ${app_name} izveidotu atkopšanas atslēgu. Nav atrasts derīgs Google Play Services APK. Paziņojumi var nedarboties pareizi. @@ -1982,9 +1980,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Marķēšana ir ieslēgta. Parāda informāciju par lietotāju Lai salabotu Matrix Apps vadību - Nomaina jūsu avatāru tikai šajā istabā + Maina Tavu iemiesojumu tikai šajā istabā Nomaina šīs istabas avatāru - Kad istabas ir atjaunotas + Kad istabas ir jauninātas Fona ierobežojumi ir ieslēgti lietotnei ${app_name}. \nDarbs, ko lietotne mēģina veikt, tiks agresīvi ierobežots, kamēr tā atrodas fonā, un tas var ietekmēt paziņojumus. \n%1$s @@ -1993,7 +1991,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Neizdevās saņemt palaišanu. Iespējams, vajag pārielādēt lietotni. Lietotne saņem Palaišanu Lietotne gaida Palaišanu - Testa Palaišana + Pārbaudīt pašpiegādi [%1$s] \nŠī kļūda ir ārpus ${app_name} kontroles. Tālrunī nav Google konta. Lūdzu, atveriet kontu pārvaldnieku un pievienojiet Google kontu. [%1$s] @@ -2004,13 +2002,13 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Atslēgvārds nevar saturēt \'%s\' Atslēgvārds nevar sākties ar \'.\' Pievienot jaunu atslēgvārdu - Jūsu atslēgvārdiem + Tavi atslēgvārdi Paziņot mani par Cits Atsauces un Atslēgvārdi Noklusējuma Paziņojumi Ieslēgt e-pasta paziņojumus priekš %s - Lai saņemtu e-pastu ar paziņojumu, lūdzu, piesaistiet e-pastu savam Matrix kontam + Lai saņemtu e-pasta paziņojumu, lūgums piesaistīt e-pasta adresi Matrix kontam E-pasta paziņojumi Nekas Tikai Atsauces un Atslēgvārdi @@ -2035,19 +2033,19 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Zvans ar %s Video zvans ar %s - Neatbildētu video zvanu - Neatbildēts video zvans + %d neatbildētu video zvanu + %d neatbildēts video zvans %d neatbildēti zvans - Neatbildētu zvanu - Neatbildēts zvans - %d neatbildēti zvans + %d neatbildētu zvanu + %d neatbildēts zvans + %d neatbildēti zvani Zvana… - Izvēlaties Mājasserveri - Mājasservera API URL adrese - Nevar sasniegt mājasserveri URL adresē %s. Lūdzu, pārbaudiet saiti vai izvēlieties mājasserveri manuāli. + Izvēlēties mājasserveri + Mājasservera API URL + Nevar sasniegt mājasserveri ar URL %s. Lūgums pārbaudīt saiti vai pašrocīgi izvēlieties mājasserveri. Izmantot pēc noklusējuma un neprasīt atkal Vienmēr prasīt Lai sūtītu balss ziņojumus, piešķiriet Mikrofona atļauju. @@ -2067,4 +2065,442 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. 2 1 Izmēģināt + + %1$d atlasīti + %1$d atlasīts + %1$d atlasīti + + %1$s nomainīja attēlojamo vārdu uz %2$s + Nav ļauts pievienoties šai istabai + Mainīt vietu + Sākotnējais sinhronizēšanas pieprasījums + Visas tērzēšanas + Izpētīt istabas + Izvērst %s apakšvienumus + + %d servera ACL izmaiņu + %d servera ACL izmaiņa + %d servera ACL izmaiņas + + Sakļaut %s apakšvienumus + Uzsākt tērzēšanu + Izveidot istabu + ${app_name} ir nepieciešams veikt kešatmiņas tīrīšanu, lai būtu pašreizēja, šī iemesla dēļ: +\n%s +\n +\nJāņem vērā, ka šī darbība pārsāknēs lietotni, un tas var aizņemt kādu laiku. + - atsevišķi lietotāji atkal tiek ņemti vērā + Ne tagad + Skatīt pavedienus + Jā, apturēt + Pamest + h + Nākamais + Nepamest neko + Izmēģināt + Uzzināt vairāk + Skatīt istabā + Ievietot pavediena saiti starpliktuvē + Atspējot + Nekad nesūtīt šifrētas ziņas uz neapstiprinātām šīs istabas sesijām. + Izkārtojuma uzstādījumi + Rādīt nesenos + Kārtot pēc + Vienalga atteikties + Iespējot + Atlasīt visu + Noņemt visas atzīmes + Pamest istabu ar norādīto identifikatoru (vai pašreizējo istabu, ja tāda nav) + Pamest visu + Vai tiešām pamest %s\? + Mājasserveris nav sasniedzams. Ja vienalga tiks veikta atteikšanās, šī ierīce netiks izdzēsta no ierīču saraksta, tādēļ nepieciešamības gadījumā tā ir noņemama citā klientā. + Tu šeit esi vienīgais cilvēks. Ja pametīsi, neviens vairs nevarēs pievienoties, ieskaitot Tevi. + Aptauju vēsture + min. + sek. + Turpināt, lai atiestatītu + Skaidrs + Rādīt atlasītājus + Darbība + Jānosūta pirmā ziņa, lai tērzēšanā uzaicinātu %s + Šeit būs redzami jaunie pieprasījumi un uzaicinājumi. + No pavediena + Vēl nav vietu. + Uzaicinājumi + Uztur apspriedes sakārtotas ar pavedieniem + Mājiens: ilgi jāpiespiež ziņa un jāizmanto “%s”. + Rāda visus pavedienus, kuros ir notikusi iesaistīšanās + Kopīgot ekrānu + Izveidot tiešo ziņu tikai pēc pirmās ziņas + Izmēģināt bagātinātu teksta rakstīšanu (vienkārša teksta ievade būs drīzumā) + Nekā jauna. + %1$s, %2$s un citi + Visi pavedieni + Mājasserveris vēl neatbalsta pavedienu uzskaitīšanu. + Rādīt nesenās tērzēšanas sistēmas kopīgošanas izvēlnē + Mani pavedieni + A - Ž + Iespējot jauno izkārtojumu + Lai sarunas pieder Tev! + ${app_name} ir nepieciešama atļauja attēlot paziņojumus. Tajos var tikt attēlotas ziņas, uzaicinājumi utt.. +\n +\nLūgums atļaut piekļuvi nākamajos uznirstošajos logos, lai varētu skatīt paziņojumu. + Ziņas šajā tērzēšanā tiks pilnībā šifrētas. + %1$s un %2$s + Tiklīdz uzaicinātie lietotāji būs pievienojušies ${app_name}, būs iespējams tērzēt, un istaba būs pilnībā šifrēta + Vis vienā drošas tērzēšanas lietotne komandām, draugiem un apvienībām. Jāizveido tērzēšana vai jāpievienojas esošai istabai, lai uzsāktu. + Šifrēšanas konfigurācijā ir kļūmes, tādēļ nevar nosūtīt ziņas. Klikšķināt, lai atvērtu iestatījumus. + Atlasīt istabas pavedienus + Pavediens + Pavedieni + Rāda pašreizējās istabas visus pavedienus + Pavedieni palīdz viegli izsekot un noturēt sarunas par tēmu. + Citi lietotāji tiešajās ziņās un istabās, kurās pievienojies, var redzēt visu Tavu sesiju sarakstu. +\n +\nTas nodrošina viņiem pārliecību, ka viņi tiešām runā ar Tevi, bet tas arī nozīmē, ka viņi var redzēt šeit ievadītās sesijas nosaukumu. + Pavedieni tuvojas Beta 🎉 + Vienkāršots Element ar cilnēm pēc izvēles + Iespējot atliktās tiešās ziņas + Vietas ir jauns veids, kā apkopot istabas un cilvēkus. Jāizveido vieta, lai sāktu. + Šifrēšanas konfigurācijā ir kļūmes, tādēļ nevar nosūtīt ziņas. Lūgums sazināties ar pārvaldītāju, lai atjaunotu šifrēšanu derīgā stāvoklī. + Atlasīt + Apturēt ekrāna kopīgošanu + Jāpārbauda sevi un citus, lai uzturētu drošas tērzēšanas + Atvērt tērzēšanu + Ar ko ir tērzēts visvairāk\? + Iespējot bagātinātu teksta rakstīšanu + E-pasta adrese nav apliecināta, jāpārbauda iesūtne + Izpētīt istabas + Pievienot esošas istabas + Vietas ir jauns veids kā apkopot istabas un cilvēkus. + Pārvaldīt istabas + Pievienot esošas istabas un vietas + Šajā vietā nav istabu + Pavedieni Beta + Dažas istabas var būt paslēptas, jo tās ir privātas un ir nepieciešams uzaicinājums. + Visas istabas, kurās ir dalība, tiks rādītas sākumsadaļā. + 🔒 Drošības iestatījumos ir iespējota tikai apliecinātu sesiju šifrēšana visās istabās. + Mēs tuvojamies publiskai pavedienu Beta izlaišanai. +\n +\nTā kā mēs tam gatavojamies, mums ir jāveic dažas izmaiņas: pirms šī brīža izveidoti pavedieni tiks attēloti kā parastas atbildes. +\n +\nŠī būs tikai vienreizēja pāreja, jo pavedieni tagad ir Matrix specifikācijā. + Pievienot istabas + Pārvaldīt istabas un vietas + Sākumsadaļā rādīt visas istabas + Dažas istabas var būt paslēptas, jo tās ir privātas un ir nepieciešams uzaicinājums. +\nNav atļaujas pievienot istabas. + Šī sesija neatbalsta šifrēšana, tādēļ to nevar apliecināt. +\n +\nNebūs iespējams piedalīties istabās, kurās ir iespējota šifrēšana, kad tiks izmantota šī sesija. +\n +\nVislabākajai drošībai un privātumam ir ieteicams izmantot Matrix klientus, kas atbalsta šifrēšanu. + Vietas ir jauns veids, kā apkopot istabas un cilvēkus. Pievienot esošu istabu vai izveidot jaunu var ar apakšējā labajā stūrī esošo pogu. + Pavedieni palīdz uzturēt sarunas par tēmu un ir viegli izsekojami. %sPavedienu iespējošana atsvaidzinās lietotni. Dažiem kontiem tas var aizņemt vairāk laika. + Pavedieni Beta + %1$s un %2$s + Palīdzēt uzlabot ${app_name} + Atskaņot kustīgos laikjoslas attēlus, tiklīdz tie ir redzami + ${app_name} ir nepieciešama atļauja, lai parādītu paziņojumus. +\nLūgums piešķirt atļauju. + + %1$s un %2$d citu + %1$s un %2$d cits + %1$s un %2$d citi + + Iespējot tiešo kopīgošanu + Istaba ir pamesta. + Ir veikta izrakstīšanās no sesijas. + Pieņemamas izmantošanas nosacījums + Mājasserveris pagaidām nenodrošina pavedienus, tādēļ šī iespēja var nebūt uzticama. Dažas pavedienu ziņas var nebūt uzreiz pieejamas. %sVienalga iespējot pavedienus\? + Konts + Automātiski atskaņot kustīgos attēlus + Atgadījās kļūda paziņojumu uzstādījumu atjaunošanā. Lūgums mēģināt vēlreiz. + Piešķirt atļauju + Galapunkta reģistrācija + Galapunkts ir veiksmīgi reģistrēts mājasserverī. + Neizdevās mājasserverī reģistrēt galapunkta pilnvaru: +\n%1$s + Crypto laidiens + Konta informācija ir pārvaldāma atsevišķi %1$s. + Vietas %1$s attēls + Istabas %1$s attēls + Lietotāja %1$s profila attēls + Iespējots: + Atbildēt pavedienā + Sesijas identifikators: + Rezerves kopijai ir derīgs šī lietotāja paraksts. + Atjauno datus… + Kaut kas nogāja greizi. Lūgums pārbaudīt tīkla savienojumu un mēģināt vēlreiz. + Bezsaistē + Klātbūtne + šeit + Diemžēl šī istaba netika atrasta. +\nLūgums vēlāk mēģināt vēl.%s + Komanda \"%s\" tika atpazīta, bet to nevar izmantot pavedienos. + Aiziet + PIEVIENOT IZVĒLES IESPĒJU + Šis pavediens jau tiek skatīts. + Palīdzi mums atrast nepilnības un uzlabot ${app_name} daloties ar anonīmiem lietojuma datiem. Lai izprastu, kā cilvēki izmanto vairākas ierīces, mēs izveidosim nejaušu identifikatoru, ko kopīgo ierīces. +\n +\nVisus mūsu nosacījumus var izlasīt %s. + Mēs <b>nedalāmies</b> ar informāciju ar trešajām pusēm + Šo jebkurā brīdī var izslēgt iestatījumos + Mājasservera nosacījumi + Identitātes servera nosacījumi + Ja iespējots, citiem lietotājiem konts būs redzams kā bezsaistē esošs pat tad, kad tiek izmantota lietotne. + ⚠ Šajā istabā ir neapliecinātas ierīces, tās nevarēs atšifrēt nosūtītās ziņas. + Izvēlēties fonta izmēru + Iestatīt automātiski + Izvēlēties pašrocīgi + Atvērt izstrādātāja rīku skatu + Skatīt istabā + Izlasē + Cilvēki + Izveidot izvēles iespējas + Visas + Nelasītās + Mēs <b>neveicam</b> nekādu konta datu ierakstīšanu un profilēšanu + ${app_name} nosacījumi + Izmantot ierīces noklusējumu + Šis serveris nesniedz nekādus nosacījumus. + Atbild uz %s + Citē + Izvēles iespēja %1$d + + Ir nepieciešamas vismaz %1$s izvēles iespēju + Ir nepieciešama vismaz %1$s izvēles iespēja + Ir nepieciešamas vismaz %1$s izvēles iespējas + + Labo + Profila birka: + Lai vienkāršotu ${app_name}, cilnes tagad ir izvēles. Tās ir pārvaldāmas ar izvēlni augšējā labajā stūrī. + Trešo pušu bibliotēkas + Pavedienu Beta atsauksme + Droša ziņapmaiņa. + Ziņapmaiņa Tavai komandai. + Pavedieni šobrīd ir izstrādē ar jaunām, aizraujošām drīzumā sagaidāmām iespējām, piemēram, uzlabotiem paziņojumiem. Mums patiktu saņemt atsauksmes. + Droša un neatkarīga saziņa, kas dod tāda paša līmeņa privātumu kā aci pret aci saruna paša mājās. + Vienā reizē var uzaicināt tikai vienu e-pasta adresi + Kontakti ir privāti. Lai atklātu lietotājus no kontaktiem, mums ir nepieciešama atļauja nosūtīt saziņas informāciju uz identitātes serveri. + Pilnīga šifrēšana, un tālruņa numurs nav nepieciešams. Bez reklāmām un datu iegūšanas. + Sūtīt e-pasta adreses un tālruņa numurus uz %s + Pievienoties esošam serverim\? + Savienoties ar serveri + Citi var atrast Tevi ar %s + Kāda ir servera adrese\? + Element Matrix Services (EMS) ir noturīgs un uzticams izvietošanas pakalpojums ātrai, drošai un reāllaika saziņai. Vairāk var uzzināt <a href=\"${ftue_ems_url}\">element.io/ems</a> + Ievadīt e-pasta adresi + Jāseko norādēm, kas tika nosūtītas uz %s + Paroles atiestatīšana + Jāseko norādēm, kas tika nosūtītas uz %s + Izskatās labi. + Profilu jebkurā brīdī var atjaunot iestatījumos + Saglabāt un turpināt + Atjaunot šifrēšanu + Starpierīču parakstīšana ir iespējota +\nPrivātās atslēgas ierīcē. + Starpierīču parakstīšana ir iespējota +\nAtslēgas ir uzticamas. +\nPrivātās atslēgas nav zināmas + Starpierīču parakstīšana ir iespējota. +\nAtslēgas nav uzticamas + Starpierīču parakstīšana nav iespējota + Saņemt palīdzību ${app_name} izmantošanā + Sistēmas iestatījumi + Izveidot jaunu sarunu vai istabu + ${app_name} ir lielisks arī darba vietā. Tam uzticas visdrošākās pasaules apvienības. + Draugi un ģimene + Kur atradīsies sarunas + Kur atrodas sarunas + Laipni lūdzam atpakaļ! + %s nosūtīs apliecinājuma saiti + Jāpārbauda e-pasts. + Jāizvēlas jauna parole + Atiestatīt paroli + Atteikties visās ierīcēs + Atkārtoti nosūtīt e-pastu + Lietotājvārds / e-pasta adrese / tālruņa numurs + Sesijas + Atvērt vietu sarakstu + Kopienas + Aiziet! + Pāriet uz sākumsadaļu + Labot + Palīdzība un atbalsts + BETA + Laidieni + Lietotne atjaunināta + Juridiski + Tu uzraugi. + Vēl nav pārliecības\? %s + E-pasta adrese + Aizmirsta parole + Attēlojamais vārds + To vēlāk var mainīt + Lūgums sazināties ar pārvaldītāju, lai atjaunotu šifrēšanu derīgā stāvoklī. + Apstiprināt tālruņa numuru + Šo saiti nevar atvērt: kopienas tika aizstātas ar vietām + Uzsāka balss pārraidi + Izlaist šo jautājumu + Apliecina ar drošo atslēgu vai vārdkopu… + Šifrēšana ir kļūdaini uzstādīta. + Palīdzība + Izvēlies, kur tiks glabātas sarunas, sniedzot vadību un neatkarību. Savienots ar Matrix. + Izlaist šo soli + Pielāgot profilu + Sazināties + Aizvietot attēlojamā vārda krāsu + Kopīgoja atrašanās vietu + Izveidot kontu + Vai esi cilvēks\? + Lūgums izlasīt %s noteikumus un nosacījumus + Lai atklātu esošus kontaktus, ir nepieciešams nosūtīt kontaktinformāciju (e-pasta adreses un tālruņa numurus) uz identitātes serveri. Privātumam pirms sūtīšanas mēs pārveidojam datus ar jaucējkodu. + Jābūt vismaz 8 rakstzīmēm + Servera nosacījumi + Atkārtoti nosūtīt kodu + BETA + Man jau ir konts + Servera URL + Jauna parole + Nolasīt kvadrātkodu + Mēs palīdzēsim savienoties + Atrašanās vieta + Balss apraide + Komandas + Apsveicam! + Jāatlasa serveris + Vēlies izvietot pats savu serveri\? + Kods tika nosūtīts uz %s + Apliecināt e-pasta adresi + Jāizvēlas attēlojamais vārds + Nebija iespējams apliecināt šo ierīci + Vai + Atiestatīt paziņojumu veidu + Jāpārliecinās, ka tajā ir vismaz 8 rakstzīmes. + Ievadīt tālruņa numuru + Tālruņa numurs + E-pasts netika saņemts\? + Laiks vārdam pievienot seju + Kopīgoja atrašanās vietu tiešraidē + Starpierīču parakstīšana + %s nepieciešams apliecināt kontu + %s nepieciešams apliecināt kontu + Apstiprinājuma kods + Vai piekrīti šīs informācijas nosūtīšanai\? + Kas ir servera adrese\? Tas ir kā mājas visiem datiem + Sniegt atsauksmi + Izvērsti žurnāla ieraksti palīdzēs izstrādātājiem, nodrošinot vairāk ierakstu, kad tiek sūtīts RageShake. Pat ja iespējots, lietotne neieraksta ziņu saturu vai jebkādu citu privātu informāciju. + Konts %s tikai izveidots + Pievienot profila attēlu + Izveidot kontu + Pieturēt, lai ierakstītu, atlaist, lai nosūtītu + Aptauja + pievieno (╯°□°)╯︵ ┻━┻ pirms vienkārša teksta ziņas + Iespējot izvērstus žurnāla ierakstus. + Kamēr lietotājs neuzticas šai sesijai, tajā nosūtītās ziņas tiks iezīmētas ar brīdinājumiem. + Šis kvadrātkods izskatās bojāts. Lūgums mēģināt apliecināt citā veidā. + Gaida lietotājus pievienojamies ${app_name} + Izveido SSSS atslēgu no atkopšanas atslēgas + Glabā atslēgas rezerves kopēšanas atslēgu SSSS + Jāievada tikai sev zināma drošības vārdkopa, kas tiek izmantota, lai noslēpumus serverī padarītu drošus. + Publicēt izveidotās identitātes atslēgas + Iegūst līknes atslēgu + Atslēgu pieprasījumi + Izveido drošu atslēgu no paroles vārdkopas + Ja atcelsi tagad, vari zaudēt šifrētās ziņas un datus, ja pazaudēsi piekļuvi pieteikšanās informācijai. +\n +\nIr arī iespējams uzstādīt drošo rezerves kopēšanu un iestatījumos pārvaldīt atslēgas. + Pārbaude rezerves atslēgu (%s) + Lūgums sazināties ar mājasservera pārvaldītāju, lai uzzinātu vairāk + Iknakts būvējums + Netika atrasts apliecināšanas pieprasījums. Tas var būt atcelts vai to jau apstrādā cita sesija. + Sinhronizē pašparakstīšanas atslēgu + Izdzēst visas kļūdainās ziņas + Šifrēšana ir nepareizi uzstādīta + Nebūs iespējams piekļūt šifrēto ziņu vēsturei. Jāatiestata Drošo ziņu rezerves kopēšana un apliecinājuma atslēgas, lai sāktu no jauna. + Iegūt jaunāko būvējumu (piezīme: var būt sarežģījumi ar pieteikšanos) + Lūgums ņemt vērā: šī ir izmēģinājuma iespēja, kas izmanto pagaidu īstenojumu. Tas nozīmē, ka nebūs iespējams izdzēst atrašanās vietu vēsturi, un prasmīgi lietotāji varēs redzēt to pat tad, kad tiks pārtraukta atrašanās vietas tiešraides kopīgošana ar šo istabu. + Šī istaba darbojas ar istabu laidienu %s, ko šis mājasserveris ir atzīmējis kā nepastāvīgu. + Drošas ziņapmaiņas uzstādīšanā atgadījās drošības nepilnība. Kaut kas no šī var būt iesaistīts drošības pārkāpumā: mājasserveris; Interneta savienojums(i); ierīce(s); + Mājasserveris nenodrošina pieteikšanos ar kvadrātkodu. + Izveido SSSS atslēgu no paroles vārdkopas (%s) + Apliecinātas sesijas ir jebkur, kur tiek izmantots šis konts pēc paroles vārdkopas ievadīšanas vai identitātes apstiprināšanas ar citu apliecinātu sesiju. +\n +\nTas nozīmē, ka ir pieejamas visas nepieciešamās atslēgas, lai atslēgtu šifrētās ziņas un apstiprinātu citiem lietotājiem, ka šī sesija ir uzticama. + Apliecināt ar citu ierīci + Jāapliecina sava identitāte, lai piekļūtu šifrētajām ziņām un pierādītu citiem savu identitāti. + Izgūt pārbaudes ierakstus + Tika nosūtīts apliecinājuma pieprasījums. Jāatver viena no savām citām sesijām, lai apstiprinātu un uzsāktu apliecināšanu. + Pārbaude rezerves atslēgu + Ja nav zināma atslēgas rezerves kopēšanas paroles vārdkopa, tad var %s. + Šifrēts izdzēstā ierīcē + Izskatās, ka Tavs mājasserveris vēl nenodrošina vietas + Izdzēst ierakstu + Krātuvē nevar atrast noslēpumus + Atslēgas rezerves kopēšanas atkopšanas atslēga + jāizmanto atslēgas rezerves kopēšanas atkopšanas atslēga + Jāievada atslēgas rezerves kopēšanas paroles vārdkopa, lai turpinātu. + Izveido SSSS atslēgu no paroles vārdkopas + Lūgums ievadīt atkopšanas atslēgu + Tā nav derīga atkopšanas atslēga + Atkļūdot + Iestata atslēgu rezerves kopēšanu + Sinhronizē lietotāja atslēgu + Sinhronizē galveno atslēgu + Iestata atkopi. + Atiestatīt atslēgas + Uzsākt starpierīču parakstīšanu + Vēlreiz jāievada drošības vārdkopa, lai to apstiprinātu. + Jāievada tikai sev zināma slepena vārdkopa un jāizveido atslēga rezerves kopēšanai. + Izmantot drošības vārdkopu + Atsākt + Nosaka SSSS noklusējuma atslēgu + Šī iestatījuma iespējošana pievieno FLAG_SECURE visām darbībām. Jāpārsāknē lietotne, lai iedarbotos izmaiņas. + Slepenā tastatūra + Mājasserveris nepieņem lietotājvārdu, kas sastāv tikai no cipariem. + Lūgums turpināt tikai tad, ja ir pārliecība, ka ir pazaudētas visas citas ierīces un drošības atslēga. + Šīs šifrētās ziņas īstumu šajā ierīcē nevar nodrošināt. + ${app_name} iOS +\n${app_name} Android + Pieprasīt, lai tastatūra neatjauno nekādus personalizētos datus, piemēram, rakstīšanas vēsturi un vārdnīcu, vadoties pēc tā, kas ir rakstīts sarunās. Jāņem vērā, ka atsevišķas tastatūras šo iestatījumu var neievērot. + ${app_name} Web +\n${app_name} Desktop + Neizdevās piekļūt drošajai krātuvei + Izmantot atkopšanas atslēgu + Piespiež pašreizējās izejošās kopuma sesijas šifrētā istabā atmešanu + Jāatlasa sava atkopšanas atslēga vai jāievada tā pašrocīgi ierakstot vai ielīmējot no starpliktuves + Apliecināšanas atslēgu atiestatīšana ir neatgriezeniska. Pēc atiestatīšanas nevarēs piekļūt iepriekšējām šifrētajām ziņā, un visi draugi, kuri iepriekš ir Tevi apliecinājuši, redzēs drošības brīdinājumus līdz atkārtotai apliecināšanai. + Drošā ziņapmaiņa tika uzlabota jaunākajā atjauninājumā. Lūgums atkārtoti apliecināt ierīci. + Tās ir jāpārskata, lai nodrošinātu, ka konts ir drošs + Tev ir neapliecinātas sesijas + Neizdevās uzstādīt starpierīču parakstīšanu + Sasaiste neizdevās. + Jāapstiprina identitāte ar šīs pieteikšanās apliecināšanu, tādējādi nodrošinot piekļuvi šifrētajām ziņām. + + Uzaicinājums tika nosūtīts %1$s + Uzaicinājumi tika nosūtīti %1$s un vēl vienam + Uzaicinājumi tika nosūtīti %1$s un vēl %2$d + + Mēs nevarējām uzaicināt lietotājus. Lūgums pārbaudīt uzaicināmos lietotājus un mēģināt vēlreiz. + Šis identitāšu serveris ir novecojis. ${app_name} nodrošina tikai API V2. + Mēs nevarējām izveidot tiešās saziņas istabu. Lūgums pārbaudīt uzaicināmos lietotājus un mēģināt vēlreiz. + Šai ziņai nevar piekļūt, jo sūtītājs ar nolūku nenosūtīja atslēgas + Nav pašreizējas sasaistes ar šo identifikatoru. + Pašpiegādes paziņojumi + Saņemt pašpiegādes paziņojumus šajā sesijā. + Nav sniegta lietotāja piekrišana. + %1$s aizliedza piekļuvi + Adrešu grāmata ir tukša + Drošā rezerves kopija + Izmantot drošības atslēgu + Izveidot drošības atslēgu, ko glabāt kādā drošā vietā, piemēram, paroļu pārvaldniekā vai seifā. + Sava drošības atslēga ir jāglabā kādā drošā vietā, piemēram, paroļu pārvaldniekā vai seifā. + Šai ziņai nevar piekļūt, jo sūtītājs Tevi ir norobežojis + Šai ziņai nevar piekļūt, jo sūtītājs neuzticas Tavai sesijai + Neizdevās atsaukt lietotāja piekļuves aizliegumu + Pašpiegādes paziņojumi ir atspējoti + Jāpārskata iestatījumi, lai iespējotu pašpiegādes paziņojumus + Saglabāt atkopšanas atslēgu + Uzstādīt \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index 31ab6c475d..f4d89b0ebd 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -3077,4 +3077,12 @@ Zaktualizowano aplikację Wyloguj mimo to Nie można skontaktować się z serwerem domowym. Jeśli mimo to się wylogujesz, urządzenie nie zostanie usunięte z listy urządzeń. Usuń je za pomocą innego klienta. + Nie można znaleźć profili dla poniższych ID Matrix. Czy chcesz rozpocząć czat mimo to\? +\n +\n%s + Zaproś mimo to + Rozpocznij czat mimo to + Nie można znaleźć profili dla poniższych ID Matrix. Czy chcesz zaprosić je mimo to\? +\n +\n%s \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index b48e72cbba..dad51116e5 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -290,7 +290,7 @@ Прогресс (%s%%) В прошлый раз приложение некорректно завершило работу. Хотите отправить отчёт о сбое\? Войти в Комнату - Имя пользователя + Псевдоним Выйти URL домашнего сервера Поиск @@ -300,7 +300,7 @@ Камера Вход Подать - Неправильное имя пользователя и/или пароль + Неверный псевдоним и/или пароль Это не похоже на действительный адрес электронной почты Этот адрес электронной почты уже используется. Забыли пароль? @@ -797,7 +797,7 @@ Создание резервной копии Сделайте резервную копию ваших ключей или потеряете доступ к вашим зашифрованным сообщениям. Уверены, что хотите выйти? - Пожалуйста, введите имя пользователя. + Пожалуйста, введите псевдоним. (Расширенный) Ручной экспорт ключей Создание резервной копии @@ -1132,11 +1132,11 @@ Международные телефонные номера должны начинаться с \'+\' Номер телефона выглядит недействительным. Пожалуйста, проверьте его Зарегистрироваться в %1$s - Имя пользователя или электронная почта - Имя пользователя + Псевдоним или электронная почта + Псевдоним Пароль Далее - Это имя пользователя занято + Этот псевдоним уже занят Предупреждение Ваш аккаунт еще не создан. Остановить процесс регистрации\? Выбрать matrix.org @@ -1144,7 +1144,7 @@ Выбрать другой сервер Пожалуйста, пройдите проверку капчей Примите условия для продолжения - Пожалуйста, проверьте ваш электронный почтовый ящик + Пожалуйста, проверьте свою электронную почту Мы только что отправили письмо на %1$s. \nПожалуйста, нажмите на содержащуюся в нём ссылку, чтобы продолжить создание аккаунта. Домашний сервер устарел @@ -1191,7 +1191,7 @@ Недоверенный вход Вложения Интерактивная проверка со смайлами - Пожалуйста, выберите имя пользователя. + Пожалуйста, выберите псевдоним. Пожалуйста, выберите пароль. Удалить… Просмотрено @@ -1549,7 +1549,7 @@ Зашифрованные сообщения в групповых чатах При обновлении комнат Посылает сообщение в виде простого текста, не интерпретируя его как разметку - Неверное имя пользователя и/или пароль. Введенный пароль начинается или заканчивается пробелами, пожалуйста, проверьте. + Неверный псевдоним и/или пароль. Введённый пароль начинается или заканчивается пробелами, пожалуйста, проверьте его. Эта учётная запись была деактивирована. Введите %s, чтобы продолжить Использовать файл @@ -2195,7 +2195,7 @@ Групповых сообщениях Зашифрованных диалогах Диалогах - Мое имя пользователя + Мой псевдоним Моё отображаемое имя Уведомлять меня о Другое @@ -2537,7 +2537,7 @@ Последняя информация о пользователе Не найдено Занят - Домашний сервер не принимает имя пользователя, состоящее только из цифр. + Домашний сервер не принимает псевдонимы, состоящие только из цифр. Пропустить этот шаг Сохранить и продолжить Ваши предпочтения были сохранены @@ -2633,7 +2633,7 @@ Должно быть 8 или более символов Не удалось заверить этот сеанс Невозможно открыть эту ссылку: сообщества были заменены пространствами - Имя пользователя / Почта / Телефон + Псевдоним / Почта / Телефон Следуйте инструкциям, отправленным на %s Забыли пароль Не получили письмо\? @@ -3020,7 +3020,7 @@ Не удалось записать голосовое сообщение Убедиться что Ваш аккаунт в безопасности Получить последнюю сборку (у вас могут быть проблемы со входом) - История опроса + Опросы Голосовая трансляция начата Ваш домашний сервер не поддерживает список обсуждений. Остановить @@ -3032,7 +3032,7 @@ Сообщение в %s Сообщение в комнате Комната/Пространство - Аккаунт + Учётная запись Аватар профиля пользователя %1$s Продолжайте, только если вы уверены, что ваш ключ утерян, а доступ к другим активным устройствам отсустствует. Пока пользователь не верифицировал эту сессию, отправленные и полученные сообщения отмечаются предупреждениями. @@ -3046,13 +3046,18 @@ При изменении настроек уведомлений произошла ошибка. Попробуйте ещё раз. Когда приглашенные пользователи присоединятся к ${app_name}, вы сможете писать им с использованием сквозного шифрования Сброс ваших ключей верификации не может быть отменен. После сброса, вы не будете иметь доступа к старым зашифрованным сообщениям, а все ваши контакты, верифицировавшие вас ранее, увидят предупреждение о повторной верификации. - Зашифрованно неактивным устройством - Защищенный обмен сообщениями был обновлен. Пожалуйста, повторно верифицируйте ваше устройство. + Зашифровано прерванным сеансом + В последнем обновлении улучшили защищённую переписку. Пожалуйста, перезаверьте свой сеанс. Не удается расшифровать голосовое сообщение. Обзор опроса во времени Сообщение - Подтвердить с помощью активного устройства + Сверить с другим сеансом Возобновить - %1$s изменил отображаемое имя на %2$s + %1$s изменил(а) имя на %2$s Запрос на верификацию не найден. Возможно, он был отменен или обработан другим сеансом. + Цитата + Версия шифрования + Блок кода + Подпункт + Пункт \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 7536c44cb7..c60927d578 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -3018,4 +3018,12 @@ Aplikácia bola aktualizovaná Aj tak sa odhlásiť Nie je možné sa spojiť s domovským serverom. Ak sa aj tak odhlásite, toto zariadenie nebude vymazané zo zoznamu zariadení, môžete ho odstrániť pomocou iného klienta. + Nie je možné nájsť profily pre nižšie uvedené Matrix ID. Chceli by ste napriek tomu začať konverzáciu\? +\n +\n%s + Spustiť konverzáciu aj tak + Nie je možné nájsť profily pre nižšie uvedené Matrix ID. Chcete ich aj tak pozvať\? +\n +\n%s + Napriek tomu pozvať \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sl/strings.xml b/library/ui-strings/src/main/res/values-sl/strings.xml index 9fec98dab7..3e392ca5a0 100644 --- a/library/ui-strings/src/main/res/values-sl/strings.xml +++ b/library/ui-strings/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ Pošlji Pridružil si se v sobi %1$s te je povabil - Odgovoril si na klic. + Odgovorili ste na klic. %s je odgovoril na klic. Spremenil si ime sobe v: %1$s %1$s je spremenil ime sobe v: %2$s @@ -40,11 +40,11 @@ Spremenil si avatarja sobe Začel si video klic. %s je začel video klic. - Začel si video klic. + Začeli ste video klic. %s je končal klic. - Končal si klic. + Končali ste klic. %1$s je %2$s omogočil ogled prihodnje zgodovine sobe - Omogočil si ogled prihodnje zgodovine sobe %1$s + Omogočili ste ogled prihodnje zgodovine sobe %1$s %1$d izbran %1$d izbrana @@ -62,7 +62,17 @@ Odstranil si svoje prikazno ime (bilo je %1$s) Temo si spremenil v: %1$s %s je začel video klic. - %1$s je omočil %2$s ogled prihodnjih sporočil - %1$s si omogočil ogled prihodnjih sporočil + %1$s je omogočil %2$s ogled prihodnjih sporočil + %1$s ste omogočili ogled prihodnjih sporočil vsi člani sobe, od trenutka povabila. + Povabilo uporabnika %s + %s je poslal(a) podatke za začetek klica. + Poslali ste podatke za začetek klica. + Tu ste izvedli nadgradnjo. + vsi člani te sobe od trenutka ko so se pridružili. + vsi člani sobe. + kdorkoli. + %s je nadgradil to sobo. + Vi ste nadgradili to sobo. + %s je tu izvedel(a) nadgradnjo. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sw/strings.xml b/library/ui-strings/src/main/res/values-sw/strings.xml index d1938c5896..dc03bffa60 100644 --- a/library/ui-strings/src/main/res/values-sw/strings.xml +++ b/library/ui-strings/src/main/res/values-sw/strings.xml @@ -1,5 +1,5 @@ - + umeondoa %1$s %1$s kuondolewa %2$s Ulikataa mwaliko @@ -11,4 +11,34 @@ %1$d Iliyochaguliwa %1$d Ziliyochaguliwa - + %1$s Andaa chumba + Umeandaa chumba + %1$s anzisha mjadala + Ulianzisha mjadala + %1$s Walioalikwa %2$s + %1$s jiunge na mjadala + %1$s Acha mjadala + Umetoka kwenye mjadala + %1$s Acha mjadala + Umeacha mjadala + %1$s hujazuiliwa %2$s + Hujazuiliwa %1$s + %1$s huruhusiwi %2$s + Huruhusiwi %1$s + Sitisha %1$s\'s mwaliko + %s\'s Mwaliko + Umejiunga na mjadala + %1$s sitisha %2$s\'s mmwaliko + Mwaliko wako + Umealika %1$s + %1$s wamebadilisha avatar + Umebadilisha avatar yako + Unaweka jina lako la kuonyesha %1$s + ulibadilisha jina lako la kuonyesha kutoka %1$s kwenda %2$s + Umeondoa jina lako la kuonyesha(lilikuwa %1$s) + %1$s alibadilisha mada kuwa: %2$s + %1$s weka jina lao la kuonyesha %2$s + %1$s badilisha majina yao kuonyesha yalitoka %2$s kwenda %3$s + %1$s badilisha majina yao kwenda %2$s + %1$s wameondoa ma jina yao yaliyonyeshwa (yalikuwa %2$s) + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index 38da26bd3d..d7f2c07a08 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -84,7 +84,7 @@ Bạn có chắc không\? Đang sao lưu khoá… Tôi không muốn các tin nhắn của tôi được mã hoá - Sao lưu Khoá Bảo mật nên được kích hoạt trên tất cả các phiên để tránh mất các tin nhắn được mã hoá. + Sao lưu bảo mật khoá nên được kích hoạt trên tất cả các phiên để tránh mất các tin nhắn được mã hoá. Đang sao lưu khoá. Nếu bạn đăng xuất bây giờ bạn sẽ không thể xem các tin nhắn được mã hoá. Bạn sẽ mất các tin nhắn được mã hoá nếu bạn đăng xuất ngay bây giờ Sử dụng Sao lưu Khóa @@ -99,7 +99,7 @@ Chủ đề tối Chủ đề sáng Quên mật khẩu\? - Đấy không giống một địa chỉ email hợp lệ + Đấy không giống một địa chỉ thư điện tử hợp lệ Tên người dùng hoặc mật khẩu không đúng Đăng nhập Xin lỗi, không có ứng dụng nào được tìm thấy có thể hoàn thành hành động này. @@ -196,7 +196,7 @@ Đã gửi Đang gửi Số điện thoại - Địa chỉ email + Địa chỉ thư điện tử Lỗi Matrix %1$s đã thay đổi cấp độ quyền lực của %2$s. Bạn đã thay đổi cấp độ quyền lực của %1$s. @@ -234,12 +234,12 @@ (ảnh đại diện cũng được thay đổi) 🎉 Tất cả máy chủ bị cấm không được tham gia! Phòng này không thể được sử dụng nữa. Không có thay đổi. - Bạn đã thay đổi ACL máy chủ cho phòng này. - %s đã thay đổi ACL máy chủ cho phòng này. + Bạn đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này. + %s đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này. • Những máy chủ khớp với %s được cho phép. • Những máy chủ khớp với %s bị cấm. - Bạn đã đặt ACL máy chủ cho phòng này. - %s đã đặt ACL máy chủ cho phòng này. + Bạn đã đặt danh sách máy chủ truy cập cho phòng này. + %s đã đặt danh sách máy chủ truy cập cho phòng này. Bạn đã nâng cấp ở đây. %s đã nâng cấp ở đây. Bạn đã nâng cấp phòng này. @@ -256,16 +256,16 @@ %1$s đã cấm %2$s Bạn đã bỏ cấm %1$s %1$s đã bỏ cấm %2$s - Bạn đã xoá tên hiển thị (nó đã là %1$s) - %1$s đã xoá tên hiển thị (nó đã là %2$s) + Bạn đã xoá tên hiển thị (từng là %1$s) + %1$s đã xoá tên hiển thị (từng là %2$s) Bạn đã đổi tên hiển thị từ %1$s thành %2$s %1$s đã đổi tên hiển thị từ %2$s thành %3$s Bạn đã đặt tên hiển thị thành %1$s %1$s đã đặt tên hiển thị thành %2$s Bạn đã thay đổi ảnh đại diện %1$s đã thay đổi ảnh đại diện - Bạn đã rút lại lời mời của %1$s - %1$s đã rút lại lời mời của %2$s + Bạn đã hủy lời mời của %1$s + %1$s đã hủy lời mời của %2$s Chủ đề của bạn Chủ đề Bạn đã kết thúc cuộc gọi. @@ -286,8 +286,8 @@ Bạn đã thay đổi chủ đề thành: %1$s Xoá các tin nhắn chưa được gửi Bạn có chắc bạn muốn xoá tất cả tin nhắn chưa được gửi trong phòng này không\? - Bạn đã kick %1$s - %1$s đã kick %2$s + Bạn đã loại bỏ %1$s + %1$s đã loại bỏ %2$s Bạn đã từ chối lời mời %1$s đã từ chối lời mời Bạn đã rời phòng @@ -329,7 +329,7 @@ %1$s đã rời phòng. Lý do: %2$s Máy chủ nhà này muốn chắc chắn bạn không phải rô bốt Số điện thoại này đã được định nghĩa rồi. - Địa chỉ email này đã được định nghĩa rồi. + Địa chỉ thư điện tử này đã được sử dụng rồi. Đăng nhập bằng đăng nhập một lần Sử dụng làm mặc định và không hỏi lại Luôn hỏi @@ -341,8 +341,8 @@ Xoá tiện ích thất bại Thêm tiện ích thất bại Chuyển - Đã xác minh - Chưa xác minh + Đã xác thực + Chưa xác thực Nhập %1$d/%2$d mã khoá thành công. @@ -359,9 +359,9 @@ Nhập các mã khoá phòng E2E Quản lý bản sao lưu mã khoá ip không xác định - Nếu chúng không khớp, sự bảo mật của việc giao tiếp của bạn có thể bị can thiệp. - Xác nhận bằng cách so sánh những điều sau đây với Cài đặt người dùng trong phiên làm việc kia của bạn: - Xác minh + Nếu chúng không khớp, bảo mật của việc giao tiếp của bạn có thể bị can thiệp. + Xác thực bằng cách so sánh những thứ sau với Cài đặt người dùng trong phiên làm việc kia của bạn: + Xác thực URL máy chủ nhà Bạn có chắc bạn muốn rời khỏi phòng không\? Rời khỏi phòng @@ -402,14 +402,14 @@ %d thay đổi thành viên Vui lòng khởi chạy ${app_name} trên một thiết bị khác mà có thể giải mã tin nhắn để nó có thể gửi các mã khoá vào phiên làm việc này. - Yêu cầu lại các mã khoá mã hoá từ các phiên làm việc khác của bạn. + Yêu cầu lại các mã khoá mã hoá từ các phiên khác của bạn. Quá nhiều yêu cầu đã được gửi Đã không chứa JSON hợp lệ Không được uỷ quyền, thiếu thông tin xác thực hợp lệ Không thể kết nối đến máy chủ nhà tại URL này, vui lòng kiểm tra nó Vui lòng nhập URL hợp lệ Vui lòng xem xét và chấp nhận chính sách của máy chủ nhà này: - Xác minh địa chỉ email thất bại: hãy chắc chắn là bạn đã nhấn vào liên kết trong email + Xác nhận địa chỉ thư điện tử thất bại: hãy chắc chắn là bạn đã nhấn vào liên kết trong email Hiện tất cả phòng trong thư mục phòng, bao gồm cả các phòng có nội dung phản cảm. Hiện các phòng có nội dung phản cảm Danh sách phòng @@ -517,8 +517,8 @@ Lời mời vào phòng • Những máy chủ khớp với IP bây giờ sẽ bị cấm. • Những máy chủ khớp với IP bây giờ sẽ được cho phép. - • Những máy chủ khớp với IP bị cấm. - • Những máy chủ khớp với IP được cho phép. + • Những máy chủ chỉ có địa chỉ Internet (IP) bị cấm. + • Những máy chủ chỉ có địa chỉ Internet (IP) được cho phép. Hủy bỏ qua người dùng Bỏ qua Bỏ qua người dùng này sẽ xóa những tin nhắn của họ khỏi những phòng bạn chia sẻ. @@ -593,7 +593,7 @@ Hủy các giới hạn Hủy tài khoản Xem lại ngay - Chìa khóa phiên + Khóa phiên Mã phiên Tên công khai Lỗi giải mã @@ -790,7 +790,7 @@ Media Chọn quốc gia Quản lý địa chỉ thư điện tử và số điện thoại liên kết với tài khoản Matrix - Email và số điện thoại + Địa chỉ thư điện tử và số điện thoại Hiện tất cả tin nhắn từ %s\? Mật khẩu của bạn vừa được cập nhật Mật khẩu này không hợp lệ @@ -800,7 +800,7 @@ Đổi mật khẩu Mật khẩu Số điện thoại này đã được sử dụng. - Địa chỉ Email này đã được sử dụng. + Địa chỉ thư điện tử này đã được sử dụng. Chọn ngôn ngữ Ngôn ngữ Giao diện người dùng @@ -878,9 +878,9 @@ Thời gian chờ giữa 2 lần đồng bộ Mã nhập vào không hợp lệ. Vui lòng kiểm tra. - Chúng tôi vừa gửi email tới %1$s. -\nClick vào đường link trong email để tiếp tục quá trình tạo tài khoản. - Hãy kiểm tra email của bạn + Chúng tôi vừa gửi thư điện tử tới %1$s. +\nNhấp vào liên kết trong thư để tiếp tục quá trình tạo tài khoản. + Hãy kiểm tra hòm thư của bạn Chấp nhận điều khoản để tiếp tục Hãy thực hiện thách thức captcha Chọn một máy chủ khác @@ -892,7 +892,7 @@ Tiếp Mật khẩu Tên đăng nhập - Username hoặc email + Tên người dùng hoặc thư điện tử Đăng ký với %1$s Số điện thoại có vẻ không hợp lệ. Hãy kiểm tra lại Số điện thoại quốc tế phải bắt đầu với dấu \'+\' @@ -908,11 +908,11 @@ Vui lòng sử dụng mẫu quốc tế. Thêm số điện thoại để tùy chọn cho phép người khác tìm bạn qua số điện thoại. Thêm số điện thoại - Tiếp - Email (tùy chọn) - Email + Tiếp tục + Địa chỉ thư điện tử (tùy chọn) + Địa chỉ thư điện tử Thêm địa chỉ thư điện tử để có thể phục hồi tài khoản. Sau này bạn có thể tùy chọn cho phép người khác tìm mình qua thông tin này. - Thêm địa chỉ email + Thêm địa chỉ thư điện tử Mật khẩu chưa được thay đổi. \n \nBạn muốn ngừng tiến trình đổi mật khẩu\? @@ -921,18 +921,18 @@ Bạn vừa đăng xuất tất cả phiên đăng nhập và không còn nhận được thông báo đẩy. Đăng nhập lại để nhận thông báo trên thiết bị. Mật khẩu của bạn đã được đặt lại. Thành công! - Tôi đã xác minh địa chỉ email + Tôi đã xác nhận địa chỉ email Nhấp vào đường dẫn để xác nhận mật khẩu mới. Sau khi bạn nhâp vào đường dẫn, hãy nhấp vào bên dưới. - Email xác thực đã được gửi tới %1$s. + Thư xác nhận đã được gửi tới %1$s. Kiểm tra mailbox Địa chỉ thư điện tử này không được liên kết với tài khoản nào Tiếp tục Đổi mật khẩu sẽ đặt lại tất cả khóa bảo mật trên tất cả phiên của bạn, làm cho lịch sử chat mã hóa không đọc được. Vui lòng Sao lưu Khóa hoặc xuất khẩu tất cả khóa bảo mật các phòng từ một phiên đăng nhập khác trước khi đặt lại mật khẩu. Cảnh báo! Mật khẩu mới - Email + Địa chỉ thư điện tử Tiếp - Email xác thực thông tin đã được gửi tới bạn để xác nhận đặt lại mật khẩu mới. + Email xác nhận đã được gửi tới bạn để xác nhận đặt lại mật khẩu mới. Đặt lại mật khẩu ở %1$s Địa chỉ thư điện tử này không được liên kết với tài khoản nào. Địa chỉ @@ -954,7 +954,7 @@ Thiết lập tùy chỉnh và nâng cao Khác Xem thêm - Giống như email, tài khoản cần có nhà riêng, dù bạn có thể nói chuyện với bất kỳ ai + Giống như tài khoản thư điện tử, tài khoản cần có nhà riêng, dù bạn có thể nói chuyện với bất kỳ ai Thiết lập tùy chỉnh. Khả dụng Thông báo không được bật cho phiên này. @@ -982,16 +982,16 @@ Thông báo mặc định Mức quan trọng của thông báo theo sự kiện Thiết lập Thông báo nâng cao - Đảm bảo rằng bạn nhấp vào đường link trong email được gửi tới bạn. + Đảm bảo rằng bạn nhấp vào đường link trong thư điện tử được gửi tới bạn. Loại bỏ %s\? Số điện thoại Không có địa chỉ thư điện tử nào trong tài khoản của bạn - Địa chỉ email + Địa chỉ thư điện tử Hiển thị thông tin ứng dụng trong thiết lập hệ thống. Thông tin ứng dụng Thêm số điện thoại Chưa có số điện thoại trong tài khoản của bạn - Thêm địa chỉ email + Thêm địa chỉ thư điện tử Tên hiển thị Hình đại diện Thêm vào màn hình Home @@ -1027,7 +1027,7 @@ Chọn vai trò được yêu cầu để thay đổi thiết lập của phòng Quyền hạn Việc này có thể có nghĩa là ai đó đang can thiệp vào lưu lượng của bạn, hoặc điện thoại của bạn không tin cậy chứng chỉ được máy chủ trên mạng cung cấp. - Không thể xác minh danh tính của máy chủ trên mạng. + Không thể xác thực danh tính của máy chủ trên mạng. Mã kiểm tra (%s): Bỏ qua Đăng xuất @@ -1060,7 +1060,7 @@ Ai có thể tìm và tham gia không gian Truy cập không gian Ai có quyền truy cập\? - Vui lòng kiểm tra email và bấm vào liên kết trong đó. Một khi xong, bấm tiếp tục. + Vui lòng kiểm tra thư điện tử và bấm vào liên kết trong đó. Một khi xong, bấm tiếp tục. Sử dụng trình quản lý chung để quản lý bot, các cầu nối, widget và các gói nhãn dán. \nTrình quản lý chung sẽ nhận được dữ liệu hiệu chỉnh, và sẽ có thể điều chỉnh các widget, gửi lời mời vào phòng và thiết lập các mốc quyền lợi theo ý bạn. ${app_name} sẽ đồng bộ hóa dưới nền trong một khoảng thời gian nhất định (có thể điều chỉnh thời gian). @@ -1115,9 +1115,9 @@ Đang chạy… (%1$d of %2$d) Chạy thử Chuẩn đoán khắc phục sự cố - Bật thông báo qua email cho %s + Bật thông báo qua thư điện tử cho %s Để được nhận thông báo qua thư điện tử, hãy liên kết một địa chỉ thư điện tử với tài khoản Matrix của bạn - Thông báo qua email + Thông báo qua thư điện tử Nâng cấp không gian Thay đổi tên không gian Bật mã hóa không gian @@ -1148,10 +1148,10 @@ Các lời mời đã gửi tới %1$s và %2$d người nữa - Hiển thị %d thiết bị bạn có thể xác minh ngay bây giờ + Hiển thị %d thiết bị bạn có thể xác thực ngay bây giờ - %d phiên đang hoạt động + %d phiên hoạt động %1$d người @@ -1271,15 +1271,15 @@ Khám phá phòng Khám phá (%s) Hoàn tất cài đặt - Mời qua email, tìm liên hệ và hơn thế nữa… + Mời qua thư điện tử, tìm liên hệ và hơn thế nữa… Hoàn tất việc cài đặt khám phá. - Hiện tại bạn không sử dụng máy chủ xác thực. Để mời đồng đội và có thể khám phá bởi họ, hãy cấu hình một bên dưới. + Hiện tại bạn không sử dụng máy chủ định danh. Để mời đồng đội và có thể khám phá bởi họ, hãy cấu hình một bên dưới. Tham gia Space Tạo space Bỏ qua ngay bây giờ Gia nhập Space của tôi %1$s %2$s Mời theo tên người dùng hoặc thư - Mời qua email + Mời qua thư điện tử Chỉ có anh lúc này thôi. %s sẽ còn tốt hơn với những người khác. Mời đến %s Mời mọi người @@ -1424,19 +1424,19 @@ Quyền Đặt quyền Xác nhận - Nhập URL của máy chủ xác thực - Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ xác thực nào khác + Nhập URL của máy chủ định danh + Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ định danh nào khác Dùng %1$s - Homeerver của bạn (%1$s) đề xuất sử dụng %2$s cho máy chủ xác thực của bạn + Máy chủ nhà của bạn (%1$s) đề xuất sử dụng %2$s cho máy chủ định danh của bạn Sự đồng ý của người dùng chưa được cung cấp. - Không có mối liên hệ hiện tại với mã định danh này. - Sự kết hợp đã thất bại. + Không có mối liên hệ hiện tại với định danh này. + Không thể liên kết. Để đảm bảo quyền riêng tư cho bạn, ${app_name} chỉ hỗ trợ gửi địa chỉ thư điện tử và số điện thoại của người dùng khi đã được băm. - Trước tiên, vui lòng chấp nhận các điều khoản của máy chủ nhận dạng trong cài đặt. - Trước tiên, vui lòng cấu hình máy chủ nhận dạng. - Hoạt động này là không thể. Homeerver đã lỗi thời. - Máy chủ nhận dạng này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2. - Ngắt kết nối khỏi máy chủ nhận dạng %s\? + Trước tiên, vui lòng chấp nhận các điều khoản của máy chủ định danh trong cài đặt. + Trước tiên, vui lòng cấu hình máy chủ định danh. + Hoạt động này là không thể. Máy chủ nhà đã lỗi thời. + Máy chủ định danh này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2. + Ngắt kết nối khỏi máy chủ định danh %s\? Mở các điều khoản của %s Tải các ngôn ngữ có sẵn… Các ngôn ngữ có sẵn khác @@ -1466,12 +1466,12 @@ Vui lòng chọn tên người dùng. Thất bại trong việc thiết lập Xác thực chéo Xác nhận danh tính của bạn bằng cách xác minh đăng nhập này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. - Xác nhận danh tính của bạn bằng cách xác minh đăng nhập này từ một trong các phiên khác của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. - Xác minh tương tác bằng Emoji - Xác minh đăng nhập - Xác minh thủ công bằng Văn bản - Xác minh thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s - Được mã hóa bởi một thiết bị chưa được xác minh + Xác thực danh tính của bạn bằng cách xác nhận đăng nhập này từ một trong các phiên khác của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Xác thực tương tác bằng Emoji + Xác thực đăng nhập + Xác thực thủ công bằng Văn bản + Xác thực thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s + Được mã hóa bởi một thiết bị chưa được xác thực Không được mã hóa gửi tuyết rơi ❄️ gửi hoa giấy 🎉 @@ -1479,7 +1479,7 @@ Gửi tin nhắn đã cho với hoa giấy Bạn sẽ khởi động lại mà không có lịch sử, không có tin nhắn, thiết bị đáng tin cậy hoặc người dùng đáng tin cậy Nếu bạn đặt lại mọi thứ - Chỉ làm điều này nếu bạn không có thiết bị nào khác mà bạn có thể xác minh thiết bị này. + Chỉ làm điều này nếu bạn không có thiết bị nào khác mà bạn có thể xác thực thiết bị này. Đặt lại mọi thứ Quên hoặc mất tất cả các tùy chọn phục hồi\? Đặt lại mọi thứ Không truy nhập được dung lượng lưu trữ an toàn @@ -1514,7 +1514,7 @@ Nó không phải là một khóa phục hồi hợp lệ Sử dụng Tệp Nhập %s của bạn để tiếp tục - Xác minh bản thân và người khác để giữ an toàn cho cuộc trò chuyện của bạn + Xác thực bản thân và người khác để giữ an toàn cho cuộc trò chuyện của bạn Nâng cấp mã hóa có sẵn Tin nhắn… Tài khoản này đã bị vô hiệu hóa. @@ -1560,7 +1560,7 @@ Mã hóa được sử dụng bởi phòng này không được hỗ trợ Mã hóa không được bật Tin nhắn trong phòng này được mã hóa đầu cuối. - Tin nhắn trong phòng này được mã hóa đầu cuối. Tìm hiểu thêm và xác minh người dùng trong hồ sơ của họ. + Tin nhắn trong phòng này được mã hóa đầu cuối. Tìm hiểu thêm và xác thực người dùng trong hồ sơ của họ. Mã hóa được bật Nếu bạn hủy ngay bây giờ, bạn có thể mất tin nhắn và dữ liệu được mã hóa nếu bạn mất quyền truy cập vào thông tin đăng nhập của mình. \n @@ -1579,9 +1579,9 @@ Phiên bản máy chủ Tên máy chủ Đăng xuất khỏi phiên này - Quản lý Phiên - Hiện Tất cả Phiên - Phiên Hoạt động + Quản lý phiên + Hiện tất cả phiên + Phiên hoạt động Người quản trị máy chủ của bạn đã vô hiệu hóa mã hóa đầu cuối theo mặc định trong phòng riêng và Tin nhắn trực tiếp. Xác thực chéo không được kích hoạt Xác thực chéo được kích hoạt. @@ -1592,12 +1592,12 @@ Xác thực chéo được kích hoạt \nKhóa riêng trên thiết bị. Xác thực chéo - Phiên mới của bạn hiện đã được xác minh. Nó có quyền truy cập vào các tin nhắn được mã hóa của bạn và những người dùng khác sẽ thấy nó đáng tin cậy. + Phiên mới của bạn hiện đã được xác thực. Nó có quyền truy cập vào các tin nhắn được mã hóa của bạn và những người dùng khác sẽ thấy nó đáng tin cậy. Tin nhắn với người dùng này được mã hóa đầu cuối và không thể được đọc bởi các bên thứ ba. So sánh mã với mã được hiển thị trên màn hình của người dùng khác. So sánh biểu tượng cảm xúc độc đáo, đảm bảo chúng xuất hiện theo cùng một thứ tự. Để được an toàn, hãy làm điều này trực tiếp hoặc sử dụng một cách khác để giao tiếp. - Để an toàn, hãy xác minh %s bằng cách kiểm tra mã một lần. + Để an toàn, hãy xác thực %s bằng cách kiểm tra mã một lần. Bật mã hóa Sau khi được bật, mã hóa cho một căn phòng không thể bị vô hiệu hóa. Tin nhắn được gửi trong một căn phòng được mã hóa không thể được nhìn thấy bởi máy chủ, chỉ bởi những người tham gia của căn phòng. Cho phép mã hóa có thể ngăn nhiều bot và cầu hoạt động chính xác. Bật mã hóa\? @@ -1607,8 +1607,8 @@ Dòng thời gian Gửi emote đã cho màu cầu vồng Gửi tin nhắn đã cho có màu cầu vồng - Phiên này không thể chia sẻ xác minh này với các phiên khác của bạn. -\nViệc xác minh sẽ được lưu cục bộ và chia sẻ trong phiên bản tương lai của ứng dụng. + Phiên này không thể chia sẻ xác thực này với các phiên khác của bạn. +\nViệc xác thực sẽ được lưu cục bộ và chia sẻ trong phiên bản tương lai của ứng dụng. Hủy bỏ qua ${app_name} gặp sự cố khi hiển thị nội dung sự kiện với id \'%1$s\' ${app_name} không xử lý các sự kiện thuộc loại \'%1$s\' @@ -1643,17 +1643,17 @@ Tin nhắn ở đây không được mã hóa đầu cuối. Tin nhắn trong phòng này không được mã hóa đầu cuối. Đang chờ %s… - Đã xác minh %s - Xác minh %s - Xác minh bằng cách so sánh biểu tượng cảm xúc - Thay vào đó, xác minh bằng cách so sánh biểu tượng cảm xúc + Đã xác thực %s + Xác thực %s + Xác thực bằng cách so sánh biểu tượng cảm xúc + Thay vào đó, xác thực bằng cách so sánh biểu tượng cảm xúc Nếu bạn không trực tiếp, hãy so sánh biểu tượng cảm xúc thay thế Không thể quét Quét bằng thiết bị này Quét mã của họ Quét mã bằng thiết bị khác của bạn hoặc chuyển đổi và quét bằng thiết bị này - Quét mã bằng thiết bị của người dùng khác để xác minh lẫn nhau một cách an toàn - Xác minh phiên này + Quét mã bằng thiết bị của người dùng khác để xác thực lẫn nhau một cách an toàn + Xác thực phiên này Yêu cầu xác minh Xác minh đã gửi Bạn đã chấp nhận @@ -1672,15 +1672,15 @@ Video. Một trong những điều sau đây có thể bị xâm phạm: \n -\n - Homeserver của bạn -\n - Homeserver mà bạn đang xác minh được kết nối với +\n - Máy chủ nhà của bạn +\n - Máy chủ nhà mà người bạn đang xác thực được kết nối với \n - Kết nối internet của bạn hoặc của người dùng khác \n - Thiết bị của bạn hoặc thiết bị của người dùng khác Không bảo mật Chúng không phù hợp Chúng phù hợp Đăng nhập không tin cậy - Tên miền email của bạn không được phép đăng ký trên máy chủ này + Tên miền địa chỉ thư điện tử của bạn không được phép đăng ký trên máy chủ này Tạo Space… Tạo phòng… Một số ký tự không được phép @@ -1749,7 +1749,7 @@ Đăng nhập bằng Matrix ID Homeerver này đang chạy một phiên bản cũ. Yêu cầu người quản trị homeerver của bạn nâng cấp. Bạn có thể tiếp tục, nhưng một số tính năng có thể không hoạt động chính xác. Homeerver lỗi thời - Không giống như một địa chỉ email hợp lệ + Không giống một địa chỉ thư điện tử hợp lệ Ứng dụng không thể tạo tài khoản trên homeerver này. \n \nBạn có muốn đăng ký bằng máy khách web không\? @@ -1769,8 +1769,8 @@ Vui lòng thử lại một khi bạn đã chấp nhận các điều khoản và điều kiện của homeserver của bạn. Nhật ký verbose sẽ giúp các nhà phát triển bằng cách cung cấp thêm nhật ký khi bạn lắc mạnh thiết bị. Ngay cả khi được bật, ứng dụng không ghi lại nội dung tin nhắn hoặc bất kỳ dữ liệu riêng tư nào khác. Bật nhật ký verbose. - Đồng ý với điều khoản dịch vụ của máy chủ xác thực (%s) để cho phép bản thân có thể khám phá bằng địa chỉ email hoặc số điện thoại. - Bạn hiện đang chia sẻ địa chỉ email hoặc số điện thoại trên máy chủ xác thực %1$s. Bạn sẽ cần kết nối lại với %2$s để ngừng chia sẻ chúng. + Đồng ý với điều khoản dịch vụ của máy chủ định danh (%s) để cho phép bản thân có thể khám phá bằng địa chỉ email hoặc số điện thoại. + Bạn hiện đang chia sẻ địa chỉ thư điện tử hoặc số điện thoại trên máy chủ định danh %1$s. Bạn sẽ cần kết nối lại với %2$s để ngừng chia sẻ chúng. Mã xác minh không chính xác. Có lỗi tra cứu số điện thoại Bàn phím số @@ -1790,24 +1790,24 @@ %1$s đã từ chối cuộc gọi này Bạn đã từ chối cuộc gọi này - Thư văn bản đã được gửi đến %s. Vui lòng nhập mã xác minh mà nó chứa. - Máy chủ xác thực bạn đã chọn không có bất kỳ điều khoản dịch vụ nào. Chỉ tiếp tục nếu bạn tin tưởng chủ sở hữu dịch vụ - Máy chủ xác thực không có điều khoản dịch vụ - Vui lòng nhập url máy chủ xác thực - Không thể kết nối với máy chủ xác thực - Nhập URL máy chủ xác thực + Tin nhắn văn bản đã được gửi đến %s. Vui lòng nhập mã xác minh mà nó chứa. + Máy chủ định danh bạn đã chọn không có bất kỳ điều khoản dịch vụ nào. Chỉ tiếp tục nếu bạn tin tưởng chủ sở hữu dịch vụ + Máy chủ định danh không có điều khoản dịch vụ + Vui lòng nhập đường dẫn máy chủ định danh + Không thể kết nối với máy chủ định danh + Nhập URL máy chủ định danh Bạn có đồng ý gửi thông tin này không\? Để khám phá các liên hệ hiện có, bạn cần gửi thông tin liên hệ (địa chỉ thư điện tử và số điện thoại) đến máy chủ định danh của mình. Chúng tôi băm dữ liệu của bạn trước khi gửi để đảm bảo quyền riêng tư. Gửi địa chỉ thư điện tử và số điện thoại đến %s Đồng ý Thu hồi sự đồng ý của tôi - Các liên hệ của bạn là riêng tư. Để khám phá người dùng từ danh bạ của bạn, chúng tôi cần sự cho phép của bạn để gửi thông tin liên hệ đến máy chủ xác thực của bạn. + Các liên hệ của bạn là riêng tư. Để khám phá người dùng từ danh bạ của bạn, chúng tôi cần sự cho phép của bạn để gửi thông tin liên hệ đến máy chủ định danh của bạn. Bạn đã đồng ý gửi địa chỉ thư điện tử và số điện thoại đến máy chủ định danh này để khám phá những người dùng khác từ danh bạ của bạn. - Gửi email và số điện thoại + Gửi thư điện tử và số điện thoại Chúng tôi đã gửi một thư đến %s, trước tiên vui lòng kiểm tra hòm thư của bạn và nhấp vào liên kết xác nhận Chúng tôi đã gửi một thư đến %s, kiểm tra hòm thư của bạn và nhấp vào liên kết xác nhận Số điện thoại có thể khám phá - Ngắt kết nối khỏi máy chủ xác thực của bạn sẽ có nghĩa là bạn sẽ không thể khám phá bởi những người dùng khác và bạn sẽ không thể mời người khác qua email hoặc điện thoại. + Ngắt kết nối khỏi máy chủ định danh của bạn sẽ có nghĩa là bạn sẽ không thể khám phá bởi những người dùng khác và bạn sẽ không thể mời người khác qua thư điện tử hoặc điện thoại. Tùy chọn Khám phá sẽ xuất hiện khi bạn đã thêm số điện thoại. Định danh ứng dụng (ID): Không có cổng Push đã đăng ký @@ -1860,12 +1860,12 @@ Các cuộc hội thoại tin nhắn trực tiếp của bạn sẽ được hiển thị tại đây. Nhấp vào dấu + dưới cùng bên phải để bắt đầu. Các cuộc hội thoại Có vẻ như bạn đang cố gắng kết nối với một homeserver khác. Bạn có muốn đăng xuất không\? - Bạn không sử dụng bất kỳ máy chủ xác thực nào + Bạn không sử dụng bất kỳ máy chủ định danh nào Lỗi không xác định - %s muốn xác minh phiên của bạn - Yêu cầu xác minh + %s muốn xác thực phiên của bạn + Yêu cầu xác thực Đã nhận được - Đã xác minh! + Đã xác thực! Chữ ký Thuật toán Phiên bản @@ -1882,16 +1882,16 @@ Xóa Sao lưu Kiểm tra trạng thái sao lưu Đang xóa bản sao lưu… - Để sử dụng Sao lưu Chính trong phiên này, hãy khôi phục bằng cụm mật khẩu hoặc khóa khôi phục của bạn ngay bây giờ. - Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác minh %s - Sao lưu có chữ ký không hợp lệ từ phiên đã xác minh %s - Sao lưu có chữ ký hợp lệ từ phiên chưa được xác minh %s - Sao lưu có chữ ký hợp lệ từ phiên đã xác minh %s. + Để sử dụng Sao lưu khóa trong phiên này, hãy khôi phục bằng cụm mật khẩu hoặc khóa khôi phục của bạn ngay bây giờ. + Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác thực %s + Sao lưu có chữ ký không hợp lệ từ phiên đã xác thực %s + Sao lưu có chữ ký hợp lệ từ phiên chưa được xác thực %s + Sao lưu có chữ ký hợp lệ từ phiên đã xác thực %s. Sao lưu có chữ ký hợp lệ từ phiên này. - Sao lưu có chữ ký từ phiên không xác định với ID %s. + Sao lưu có chữ ký từ phiên không xác định với định danh %s. Khóa của bạn không được sao lưu từ phiên này. - Sao lưu chính không hoạt động trong phiên này. - Key Backup đã được thiết lập chính xác cho phiên này. + Sao lưu khóa không hoạt động trong phiên này. + Sao lưu khóa đã được thiết lập chính xác cho phiên này. Xóa Sao lưu Khôi phục từ Sao lưu Thất bại trong việc nhận được phiên bản khóa khôi phục mới nhất (%s). @@ -1968,17 +1968,17 @@ Một trong những điều sau đây có thể bị xâm phạm: \n \n- Mật khẩu của bạn -\n- Người ở nhà của anh -\n- Thiết bị này hoặc thiết bị khác +\n- Máy chủ nhà của bạn +\n- Thiết bị này hoặc thiết bị kia \n- Kết nối internet mà một trong hai thiết bị đang sử dụng \n \nChúng tôi khuyên bạn nên thay đổi mật khẩu và khóa khôi phục trong Cài đặt ngay lập tức. - Bạn sẽ không xác minh %1$s (%2$s) nếu bạn hủy ngay. Bắt đầu lại trong hồ sơ người dùng của họ. + Bạn sẽ không xác thực %1$s (%2$s) nếu bạn hủy ngay. Bắt đầu lại trong hồ sơ người dùng của họ. Nếu bạn hủy, bạn sẽ không thể đọc tin nhắn được mã hóa trên thiết bị mới của mình và những người dùng khác sẽ không tin tưởng nó Nếu bạn hủy, bạn sẽ không thể đọc tin nhắn được mã hóa trên thiết bị này và những người dùng khác sẽ không tin tưởng nó Tài khoản của bạn có thể bị xâm phạm Không phải tôi - Sử dụng phiên này để xác minh phiên mới của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Sử dụng phiên này để xác thực phiên mới của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. Đăng nhập mới. Đây có phải là bạn không\? Làm tươi Mở khóa lịch sử tin nhắn được mã hóa @@ -2011,20 +2011,20 @@ Mã QR Đặt lại khóa Khởi tạo xác thực chéo - Cho đến khi người dùng này tin tưởng phiên này, tin nhắn được gửi đến nó và từ nó đều mang nhãn cảnh báo. Ngoài ra, bạn có thể xác minh thủ công. + Cho đến khi người dùng này tin tưởng phiên này, tin nhắn được gửi đến nó và từ nó đều mang nhãn cảnh báo. Ngoài ra, bạn có thể xác thực thủ công. %1$s (%2$s) đã đăng nhập bằng phiên mới: - Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác minh: + Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác thực: Không tin cậy Tin cậy Phiên Không nhận được phiên Cảnh báo - Đã xác minh - Xác minh - Sử dụng phiên hiện có để xác minh phiên này, cấp cho nó quyền truy cập vào các thư được mã hóa. - Xác minh đăng nhập này - Xác minh phiên này để đánh dấu nó là đáng tin cậy và cấp cho nó quyền truy cập vào các thư được mã hóa. Nếu bạn không đăng nhập vào phiên này, tài khoản của bạn có thể bị xâm phạm: - Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác minh nó: + Đã xác thực + Xác thực + Sử dụng phiên hiện có để xác thực phiên này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Xác thực thiết bị này + Xác thực phiên này để đánh dấu nó là đáng tin cậy và cấp cho nó quyền truy cập vào các thư được mã hóa. Nếu bạn không đăng nhập vào phiên này, tài khoản của bạn có thể bị xâm phạm: + Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác thực nó: Không có thông tin mật mã sẵn dùng bất ổn định ổn định @@ -2058,9 +2058,9 @@ Vui lòng nhập tên người dùng. Hủy kích hoạt Tài khoản Vui lòng quên tất cả các tin nhắn tôi đã gửi khi tài khoản của tôi bị vô hiệu hóa (Cảnh báo: điều này sẽ khiến người dùng trong tương lai thấy chế độ xem cuộc hội thoại không đầy đủ) - Điều này sẽ làm cho tài khoản của bạn vĩnh viễn không thể sử dụng được. Bạn sẽ không thể đăng nhập và không ai có thể đăng ký lại cùng một ID người dùng. Điều này sẽ khiến tài khoản của bạn rời khỏi tất cả các phòng mà nó đang tham gia và nó sẽ xóa chi tiết tài khoản của bạn khỏi máy chủ nhận dạng của bạn. Điều này là không thể đảo ngược. + Điều này sẽ làm cho tài khoản của bạn vĩnh viễn không thể sử dụng được. Bạn sẽ không thể đăng nhập và không ai có thể đăng ký lại cùng một ID người dùng. Điều này sẽ khiến tài khoản của bạn rời khỏi tất cả các phòng mà nó đang tham gia và nó sẽ xóa chi tiết tài khoản của bạn khỏi máy chủ định danh của bạn. <b>Điều này là không thể đảo ngược</b>. \n -\nHủy kích hoạt tài khoản của bạn does không theo mặc định khiến chúng tôi quên tin nhắn bạn đã gửi. Nếu bạn muốn chúng tôi quên tin nhắn của bạn, vui lòng đánh dấu vào hộp bên dưới. +\nHủy kích hoạt tài khoản của bạn <b>does không theo mặc định khiến chúng tôi quên tin nhắn bạn đã gửi</b>. Nếu bạn muốn chúng tôi quên tin nhắn của bạn, vui lòng đánh dấu vào hộp bên dưới. \n \nKhả năng hiển thị tin nhắn trong Matrix tương tự như email. Chúng tôi quên tin nhắn của bạn có nghĩa là tin nhắn bạn đã gửi sẽ không được chia sẻ với bất kỳ người dùng mới hoặc chưa đăng ký nào, nhưng người dùng đã đăng ký đã có quyền truy cập vào các tin nhắn này vẫn sẽ có quyền truy cập vào bản sao của họ. Để tiếp tục sử dụng homeserver %1$s, bạn phải xem xét và đồng ý với các điều khoản và điều kiện. @@ -2102,11 +2102,11 @@ Lệnh không được nhận ra: %s Lỗi lệnh Yêu cầu Chia sẻ Khóa - Một phiên chưa được xác minh đang yêu cầu khóa mã hóa. + Một phiên chưa được xác thực đang yêu cầu khóa mã hóa. \nTên phiên: %1$s \nLần nhìn thấy lần cuối: %2$s \nNếu bạn không đăng nhập vào phiên khác, hãy bỏ qua yêu cầu này. - Phiên chưa được xác thực của bạn \'%s\' đang yêu cầu khóa mã hóa. + Phiên chưa được xác thực \'%s\' đang yêu cầu khóa mã hóa. Một phiên mới đang yêu cầu các khóa mã hóa. \nTên phiên: %1$s \nLần nhìn thấy lần cuối: %2$s @@ -2182,13 +2182,13 @@ Tất cả các phòng trên %s server Tên máy chủ Chọn thư mục phòng - Không bao giờ gửi tin nhắn được mã hóa đến các phiên chưa được xác minh từ phiên này. - Chỉ mã hóa cho các phiên đã xác minh + Không bao giờ gửi tin nhắn được mã hóa đến các phiên chưa được xác thực từ phiên này. + Chỉ mã hóa cho các phiên đã xác thực Bỏ đặt làm địa chỉ chính Đặt làm địa chỉ chính Máy chủ này không cung cấp bất kỳ chính sách nào. Thư viện bên thứ ba - Chính sách máy chủ xác thực của bạn + Chính sách máy chủ định danh của bạn Chính sách homeerver của bạn Chính sách ${app_name} Bạn có thể tắt tính năng này bất cứ lúc nào trong cài đặt @@ -2199,7 +2199,7 @@ \n \nBạn có thể đọc tất cả các thuật ngữ của chúng tôi %s. Giúp cải thiện ${app_name} - Thiết bị đã bị đăng xuất! + Phiên đã bị đăng xuất! Căn phòng đã bị bỏ lại! Chọn homeerver Không thể kết nối đến một homeserver tại URL %s. Vui lòng kiểm tra liên kết của bạn hoặc chọn homeerver thủ công. @@ -2207,11 +2207,11 @@ Kích hoạt Nghe thông báo Tùy chọn Khám phá sẽ xuất hiện sau khi bạn đã thêm địa chỉ thư điện tử. - Địa chỉ email có thể khám phá - Hiện tại bạn không sử dụng máy chủ xác thực. Để khám phá và có thể khám phá bởi các liên hệ hiện có mà bạn biết, hãy cấu hình một danh bạ dưới đây. - Không có chính sách được cung cấp bởi máy chủ xác thực - Ẩn chính sách máy chủ xác thực - Hiện chính sách máy chủ xác thực + Địa chỉ thư điện tử có thể khám phá + Hiện tại bạn không sử dụng máy chủ định danh. Để khám phá và có thể khám phá bởi các liên hệ hiện có mà bạn biết, hãy cấu hình một danh bạ dưới đây. + Máy chủ định danh không cung cấp chính sách nào + Ẩn chính sách máy chủ định danh + Hiện chính sách máy chủ định danh Mở Cài đặt Khám phá Thêm tab dành riêng cho các thông báo chưa đọc trên màn hình chính. Tìm kiếm theo tên, ID hoặc thư @@ -2334,7 +2334,7 @@ Bạn có thể đổi lại sau Tên hiển thị Chọn tên hiển thị - Tên người dùng / Thư điện tử / Số điện thoại + Tên người dùng / Địa chỉ thư điện tử / Số điện thoại Bạn có phải con người\? Làm theo chỉ dẫn gửi tới %s Đặt lại mật khẩu @@ -2376,7 +2376,7 @@ Gửi phản hồi Đã bật: Thẻ hồ sơ: - Định danh của phiên: + Định danh phiên: Đi Đang cập nhật dữ liệu của bạn… Mọi người @@ -2477,7 +2477,7 @@ Chính sách máy chủ %s sẽ gửi bạn một liên kết xác nhận Làm chủ cuộc trò chuyện. - %s cần xác nhận tài khoản của bạn + %s cần xác thực tài khoản của bạn Chào mừng trở lại! Địa chỉ máy chủ của bạn là gì\? Chúng tôi sẽ giúp bạn kết nối @@ -2531,4 +2531,27 @@ Ẩn địa chỉ Internet (IP) Phiên đã xác thực Hệ điều hành + Bộ lọc + Bộ lọc các chủ đề trong phòng + Sao chép liên kết vào cuộc trò chuyện + Các chủ đề + Bản Beta của Chủ đề + Bản Beta của Chủ đề + Các chủ đề đã chuyển sang Beta 🎉 + Hiển thị tất cả chủ đề trong phòng + Tất cả chủ đề + Sắp xếp các cuộc thảo luận với các chủ đề + Chủ đề + Hiển thị tất cả chủ đề bạn đã tham gia + Element đã được đơn giản hóa với các mục tùy chọn + Chỉ tạo cuộc trò chuyện riêng cho tin nhắn đầu tiên + Bật trì hoãn cho các tin nhắn riêng + Các chủ đề của tôi + Từ một chủ đề + \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. + Máy chủ nhà của bạn chưa hỗ trợ cho việc liệt kê các chủ đề. + \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. %sBật chủ đề sẽ làm mới ứng dụng. Quá trình này có thể mất nhiều thời gian hơn đối với một số tài khoản. + Phiên + Thiết bị + Hoạt động cuối %1$s \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 33168cdc75..9abc3b25e4 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -1734,7 +1734,7 @@ 在消息框添加打开 emoji 键盘的按钮 显示 emoji 键盘 使用 /confetti 命令或发送包含 ❄️ 或 🎉 的消息 - 显示聊天效果 + 显示聊天特效 更改话题 升级房间 发送 m.room.server_acl 事件 @@ -2846,4 +2846,23 @@ 你一次仅能邀请一个电子邮件 开始语音广播 继续 + 已结束投票 + 投票 + 过去的投票 + 消息 + 加载更多投票 + 链接 + 创建链接 + 编辑链接 + 切换无序列表 + 房间/空间 + 进行中的投票 + 取消缩进 + 文本 + 已结束投票。 + 缩进 + 设置链接 + 显示投票 + 切换引用 + 切换有序列表 \ No newline at end of file diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b9e9a71d75..6b29e56d77 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.3\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.5\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -189,7 +189,7 @@ dependencies { // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' - kapt 'dk.ilios:realmfieldnameshelper:2.0.0' + kapt project(":library:external:realmfieldnameshelper") // Shared Preferences implementation libs.androidx.preferenceKtx diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 4968df775a..78672adb00 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -77,9 +77,9 @@ data class HomeServerCapabilities( val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, /** - * True if the home server supports event redaction with relations. + * True if the home server supports redaction of related events. */ - var canRedactEventWithRelations: Boolean = false, + var canRedactRelatedEvents: Boolean = false, /** * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 07036f4b65..9eb0fa4097 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -158,10 +158,10 @@ interface SendService { * Redact (delete) the given event. * @param event the event to redact * @param reason optional reason string - * @param withRelations the list of relation types to redact with this event + * @param withRelTypes the list of relation types to redact with this event * @param additionalContent additional content to put in the event content */ - fun redactEvent(event: Event, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Cancelable + fun redactEvent(event: Event, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Cancelable /** * Schedule this message to be resent. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 3fe5541b68..83186344bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -59,8 +59,7 @@ private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS = "org.matrix.msc3912" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE = "org.matrix.msc3912.stable" +private const val FEATURE_REDACTION_OF_RELATED_EVENT = "org.matrix.msc3912" /** * Return true if the SDK supports this homeserver version. @@ -162,9 +161,8 @@ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolea /** * Indicate if the server supports MSC3912: https://github.com/matrix-org/matrix-spec-proposals/pull/3912. * - * @return true if event redaction with relations is supported + * @return true if redaction of related events is supported */ -internal fun Versions.doesServerSupportRedactEventWithRelations(): Boolean { - return unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS).orFalse() || - unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE).orFalse() +internal fun Versions.doesServerSupportRedactionOfRelatedEvents(): Boolean { + return unstableFeatures?.get(FEATURE_REDACTION_OF_RELATED_EVENT).orFalse() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt index b060748a61..01d59a8c80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt @@ -30,7 +30,7 @@ internal interface RedactEventTask : Task { val roomId: String, val eventId: String, val reason: String?, - val withRelations: List?, + val withRelTypes: List?, ) } @@ -41,9 +41,9 @@ internal class DefaultRedactEventTask @Inject constructor( ) : RedactEventTask { override suspend fun execute(params: RedactEventTask.Params): String { - val withRelations = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactEventWithRelations.orFalse() && - !params.withRelations.isNullOrEmpty()) { - params.withRelations + val withRelTypes = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactRelatedEvents.orFalse() && + !params.withRelTypes.isNullOrEmpty()) { + params.withRelTypes } else { null } @@ -55,7 +55,7 @@ internal class DefaultRedactEventTask @Inject constructor( eventId = params.eventId, body = EventRedactBody( reason = params.reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 1c7a0591a1..b566cf7cb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -47,7 +47,7 @@ internal object HomeServerCapabilitiesMapper { canLoginWithQrCode = entity.canLoginWithQrCode, canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, - canRedactEventWithRelations = entity.canRedactEventWithRelations, + canRedactRelatedEvents = entity.canRedactEventWithRelations, externalAccountManagementUrl = entity.externalAccountManagementUrl, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index a368325793..71f6ebd6e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin -import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactEventWithRelations +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads @@ -154,7 +154,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() homeServerCapabilitiesEntity.canRedactEventWithRelations = - getVersionResult.doesServerSupportRedactEventWithRelations() + getVersionResult.doesServerSupportRedactionOfRelatedEvents() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 7176e36e0c..04749103c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -140,11 +140,11 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun redactEvent(event: Event, reason: String?, withRelations: List?, additionalContent: Content?): Cancelable { + override fun redactEvent(event: Event, reason: String?, withRelTypes: List?, additionalContent: Content?): Cancelable { // TODO manage media/attachements? - val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelations, additionalContent) + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelTypes, additionalContent) .also { createLocalEcho(it) } - return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelations) + return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelTypes) } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 3259120cd6..70c4de921a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -830,12 +830,12 @@ internal class LocalEchoEventFactory @Inject constructor( } } */ - fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Event { + fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Event { val localId = LocalEcho.createLocalEchoId() - val content = if (reason != null || withRelations != null) { + val content = if (reason != null || withRelTypes != null) { EventRedactBody( reason = reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ).toContent().plus(additionalContent.orEmpty()) } else { additionalContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index 576f31c64c..270d3a228e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -43,7 +43,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses val roomId: String, val eventId: String, val reason: String?, - val withRelations: List? = null, + val withRelTypes: List? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -63,7 +63,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses roomId = params.roomId, eventId = params.eventId, reason = params.reason, - withRelations = params.withRelations, + withRelTypes = params.withRelTypes, ) ) }.fold( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt index cf2bc0dc4f..2ed5c9f363 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt @@ -25,5 +25,10 @@ internal data class EventRedactBody( val reason: String? = null, @Json(name = "org.matrix.msc3912.with_relations") - val withRelations: List? = null, -) + val unstableWithRelTypes: List? = null, + + @Json(name = "with_rel_types") + val withRelTypes: List? = null, +) { + fun getBestWithRelTypes() = withRelTypes ?: unstableWithRelTypes +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index b285e90c9a..90d78a51e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -26,9 +26,9 @@ internal interface EventSenderProcessor : SessionLifecycleObserver { fun postEvent(event: Event, encrypt: Boolean): Cancelable - fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List? = null): Cancelable - fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelTypes: List? = null): Cancelable fun postTask(task: QueuedTask): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 9ce29c3c08..a4e3773eb9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -101,8 +101,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor( return postTask(task) } - override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List?): Cancelable { - return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelations) + override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List?): Cancelable { + return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelTypes) } override fun postRedaction( @@ -110,9 +110,9 @@ internal class EventSenderProcessorCoroutine @Inject constructor( eventToRedactId: String, roomId: String, reason: String?, - withRelations: List? + withRelTypes: List? ): Cancelable { - val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelations) + val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelTypes) return postTask(task) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index a900e4ae5d..85238ae944 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -118,7 +118,7 @@ internal class QueueMemento @Inject constructor( eventId = it.redacts, roomId = it.roomId, reason = body?.reason, - withRelations = body?.withRelations, + withRelTypes = body?.getBestWithRelTypes(), ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt index 46df7e29f3..e79808ee3f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt @@ -43,13 +43,13 @@ internal class QueuedTaskFactory @Inject constructor( ) } - fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelations: List? = null): QueuedTask { + fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelTypes: List? = null): QueuedTask { return RedactQueuedTask( redactionLocalEchoId = redactionLocalEcho, toRedactEventId = eventId, roomId = roomId, reason = reason, - withRelations = withRelations, + withRelTypes = withRelTypes, redactEventTask = redactEventTask, localEchoRepository = localEchoRepository, cancelSendTracker = cancelSendTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index f484c24aae..b51a04f863 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -26,14 +26,14 @@ internal class RedactQueuedTask( val redactionLocalEchoId: String, private val roomId: String, private val reason: String?, - private val withRelations: List?, + private val withRelTypes: List?, private val redactEventTask: RedactEventTask, private val localEchoRepository: LocalEchoRepository, private val cancelSendTracker: CancelSendTracker ) : QueuedTask(queueIdentifier = roomId, taskIdentifier = redactionLocalEchoId) { override suspend fun doExecute() { - redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelations)) + redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelTypes)) } override fun onTaskFailed() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt index 3700bbf46f..3c205d5013 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository import org.matrix.android.sdk.internal.di.SessionId @@ -81,7 +82,9 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor( } } if (hasUpdate) { - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + tryOrNull("Unable to update user account data") { + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 92ebb41ad9..bc49682565 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmList import io.realm.kotlin.where +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.InitialSyncRequestReason import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent @@ -122,7 +123,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( directMessages = directChats ) - updateUserAccountDataTask.execute(updateUserAccountParams) + tryOrNull("Unable to update user account data") { updateUserAccountDataTask.execute(updateUserAccountParams) } } } diff --git a/settings.gradle b/settings.gradle index ea20f12175..a0b9ce65ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,12 @@ include ':library:multipicker' include ':library:external:jsonviewer' include ':library:external:diff-match-patch' include ':library:external:dialpad' +include ':library:external:textdrawable' +include ':library:external:autocomplete' +include ':library:external:realmfieldnameshelper' +include ':library:external:span' +include ':library:external:barcodescanner:core' +include ':library:external:barcodescanner:zxing' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 7e4147045f..e099145f02 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 3 +ext.versionPatch = 5 ext.scVersion = 69 @@ -394,6 +394,7 @@ dependencies { implementation project(':vector') implementation project(':vector-config') implementation project(':library:core-utils') + debugImplementation project(':library:external:span') debugImplementation project(':library:ui-styles') implementation libs.dagger.hilt implementation 'androidx.multidex:multidex:2.0.1' diff --git a/vector/build.gradle b/vector/build.gradle index 3c86e9c1d6..1456be784b 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -116,6 +116,8 @@ dependencies { implementation project(":matrix-sdk-android-flow") implementation project(":library:external:jsonviewer") implementation project(":library:external:diff-match-patch") + implementation project(":library:external:textdrawable") + implementation project(":library:external:autocomplete") implementation project(":library:ui-strings") implementation project(":library:ui-styles") implementation project(":library:core-utils") @@ -186,11 +188,8 @@ dependencies { api libs.androidx.preferenceKtx // UI - implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation libs.google.material - api('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.markwon.core implementation libs.markwon.extLatex implementation libs.markwon.imageGlide @@ -212,8 +211,6 @@ dependencies { // Alerter implementation 'com.github.tapadoo:alerter:7.2.4' - implementation 'com.otaliastudios:autocomplete:1.1.0' - // Shake detection implementation 'com.squareup:seismic:1.0.3' @@ -269,11 +266,7 @@ dependencies { // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 implementation 'com.google.zxing:core:3.3.3' - // Excludes the legacy support library annotation usages - // https://github.com/dm77/barcodescanner/blob/d036996c8a6f36a68843ffe539c834c28944b2d5/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java#L4 - implementation ('me.dm7.barcodescanner:zxing:1.9.13') { - exclude group: 'com.android.support', module: 'support-v4' - } + implementation project(":library:external:barcodescanner:zxing") // Emoji Keyboard api libs.vanniktech.emojiMaterial diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt index 7625eb6216..cd7ecb8a00 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt @@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.otaliastudios.autocomplete.AutocompletePresenter -abstract class RecyclerViewPresenter(context: Context?) : AutocompletePresenter(context) { +abstract class RecyclerViewPresenter(context: Context) : AutocompletePresenter(context) { private var recyclerView: RecyclerView? = null private var clicks: ClickProvider? = null diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt index 08f61be0f8..9097d2be43 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -32,16 +32,15 @@ class CommandAutocompletePolicy @Inject constructor() : AutocompletePolicy { return "" } - override fun onDismiss(text: Spannable?) { + override fun onDismiss(text: Spannable) { } // Only if text which starts with '/' and without space - override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean { - return enabled && text?.startsWith("/") == true && - !text.contains(" ") + override fun shouldShowPopup(text: Spannable, cursorPos: Int): Boolean { + return enabled && text.startsWith("/") && !text.contains(" ") } - override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean { + override fun shouldDismissPopup(text: Spannable, cursorPos: Int): Boolean { return !shouldShowPopup(text, cursorPos) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 395b4d0475..fd61bdd5dd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction import java.io.OutputStream sealed class BootstrapActions : VectorViewModelAction { + object Retry : BootstrapActions() // Navigation diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index be02737ef8..586f60beaf 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -212,6 +212,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment { + views.bootstrapIcon.isVisible = true + views.bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title) + showFragment(BootstrapErrorFragment::class) + } } super.invalidate() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt new file mode 100644 index 0000000000..26b29d449a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.crypto.recover + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentBootstrapErrorBinding + +@AndroidEntryPoint +class BootstrapErrorFragment : + VectorBaseFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapErrorBinding { + return FragmentBootstrapErrorBinding.inflate(inflater, container, false) + } + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun invalidate() = withState(sharedViewModel) { state -> + when (state.step) { + is BootstrapStep.Error -> { + views.bootstrapDescriptionText.setTextOrHide(errorFormatter.toHumanReadable(state.step.error)) + } + else -> { + // Should not happen, show a generic error + views.bootstrapDescriptionText.setTextOrHide(getString(R.string.unknown_error)) + } + } + views.bootstrapRetryButton.onClick { + sharedViewModel.handle(BootstrapActions.Retry) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index b3d83b948b..c9c2c5ce9a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromR import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber import java.io.OutputStream import kotlin.coroutines.Continuation import kotlin.coroutines.resumeWithException @@ -118,37 +119,48 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } SetupMode.NORMAL -> { - // need to check if user have an existing keybackup - setState { - copy(step = BootstrapStep.CheckingMigration) - } + checkMigration() + } + } + } - // We need to check if there is an existing backup - viewModelScope.launch(Dispatchers.IO) { - val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }?.toKeysVersionResult() - if (version == null) { - // we just resume plain bootstrap - doesKeyBackupExist = false + private fun checkMigration() { + // need to check if user have an existing keybackup + setState { + copy(step = BootstrapStep.CheckingMigration) + } + + // We need to check if there is an existing backup + viewModelScope.launch(Dispatchers.IO) { + try { + val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }?.toKeysVersionResult() + if (version == null) { + // we just resume plain bootstrap + doesKeyBackupExist = false + setState { + copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) + } + } else { + // we need to get existing backup passphrase/key and convert to SSSS + val keyVersion = tryOrNull { + session.cryptoService().keysBackupService().getVersion(version.version) + } + if (keyVersion == null) { + // strange case... just finish? + _viewEvents.post(BootstrapViewEvents.Dismiss(false)) + } else { + doesKeyBackupExist = true + isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null setState { copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) } - } else { - // we need to get existing backup passphrase/key and convert to SSSS - val keyVersion = tryOrNull { - session.cryptoService().keysBackupService().getVersion(version.version) - } - if (keyVersion == null) { - // strange case... just finish? - _viewEvents.post(BootstrapViewEvents.Dismiss(false)) - } else { - doesKeyBackupExist = true - isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null - setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) - } - } } } + } catch (failure: Throwable) { + Timber.e(failure, "Error while checking key backup") + setState { + copy(step = BootstrapStep.Error(failure)) + } } } } @@ -268,6 +280,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error))) } } + BootstrapActions.Retry -> { + checkMigration() + } } } @@ -568,6 +583,12 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } + is BootstrapStep.Error -> { + // do we let you cancel from here? + if (state.canLeave) { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index 3975f0e9a2..b807acc0ba 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -105,6 +105,8 @@ sealed class BootstrapStep { object Initializing : BootstrapStep() data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() object DoneSuccess : BootstrapStep() + + data class Error(val error: Throwable) : BootstrapStep() } fun BootstrapStep.GetBackupSecretForMigration.useKey(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index fde8a8b24f..3775c966d9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerC import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap +import timber.log.Timber import java.io.File import java.net.URL import java.util.UUID @@ -269,7 +270,17 @@ class VectorSettingsGeneralFragment : // Disable it while updating the state, will be re-enabled by the account data listener. it.isEnabled = false lifecycleScope.launch { - session.integrationManagerService().setIntegrationEnabled(newValue as Boolean) + try { + session.integrationManagerService().setIntegrationEnabled(newValue as Boolean) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to update integration manager state") + activity?.let { activity -> + Toast.makeText(activity, errorFormatter.toHumanReadable(failure), Toast.LENGTH_SHORT).show() + } + // Restore the previous state + it.isChecked = !it.isChecked + it.isEnabled = true + } } true } diff --git a/vector/src/main/res/layout/fragment_bootstrap_error.xml b/vector/src/main/res/layout/fragment_bootstrap_error.xml new file mode 100644 index 0000000000..02c944fed2 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_error.xml @@ -0,0 +1,33 @@ + + + + + +