diff --git a/CHANGES.rst b/CHANGES.rst index e82f433b49..e3a2903e96 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,25 @@ +Changes in Riot 0.9.9 (2019-11-25) +=================================================== + +MatrixSdk 🚀: + - Upgrade to version 0.9.32 + - Changelog: https://github.com/matrix-org/matrix-android-sdk/releases/tag/v0.9.32 + +Features ✨: + - Privacy / Room Widget permissions (#3378) + - Privacy / Widget Permission for jitsi widgets (#3391) + +Improvements 🙌: + - Jitsi / Use mx display name in Jitsi conf + +Other changes: + - Add User-Interactive Auth to /account/3pid/add (#3333) + +Bugfix 🐛: + - Crash / potential NPE after logout (#3367) + - Fix infinite restart loop after token expiration (#3249) + + Changes in Riot 0.9.8 (2019-10-09) =================================================== diff --git a/README.md b/README.md index 4e361a3abd..15ac478e50 100755 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Riot-Android [![Buildkite](https://badge.buildkite.com/5ae4f24dd485562a5b59a9f84 Important Announcement ====================== -The core team is now working mainly on [RiotX](https://github.com/vector-im/riotX-android). New contributions (PR, issues) are still welcome, but be aware that this codebase will be replaced in the future by the RiotX implementation. +The core team is now working mainly on [RiotX](https://github.com/vector-im/riotX-android). New contributions about security concerns (PR, issues) are still welcome. Other subjects may rarely be addressed, as we do not have time to spend on maintenance on new features. Please contribute to RiotX now! Contributing ============ diff --git a/build.gradle b/build.gradle index 2b84392915..109ac0de04 100755 --- a/build.gradle +++ b/build.gradle @@ -23,8 +23,8 @@ buildscript { // global properties used in sub modules ext { - versionCodeProp = 90800 - versionNameProp = "0.9.8" + versionCodeProp = 90900 + versionNameProp = "0.9.9" versionBuild = System.getenv("BUILD_NUMBER") as Integer ?: 0 buildNumberProp = "${versionBuild}" } diff --git a/vector/build.gradle b/vector/build.gradle index 2c8ab2581b..b0abb11d80 100755 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -245,7 +245,7 @@ dependencies { /************* Matrix SDK management **************/ // update settings.gradle // use the matrix SDK as external dependency - implementation 'com.github.matrix-org:matrix-android-sdk:v0.9.30' + implementation 'com.github.matrix-org:matrix-android-sdk:v0.9.32' // use the matrix SDK as a sub project // you have to uncomment some lines in settings.gradle //implementation project(':matrix-sdk') diff --git a/vector/src/main/java/im/vector/Matrix.java b/vector/src/main/java/im/vector/Matrix.java index 8ba21131d7..6a7d75caaf 100755 --- a/vector/src/main/java/im/vector/Matrix.java +++ b/vector/src/main/java/im/vector/Matrix.java @@ -117,6 +117,8 @@ public class Matrix { @Nullable private KeyRequestHandler mKeyRequestHandler; + private Map mWidgetManagerProviders = new HashMap<>(); + // i.e the event has been read from another client private static final MXEventListener mLiveEventListener = new MXEventListener() { boolean mClearCacheRequired = false; @@ -133,10 +135,12 @@ public void onIgnoredUsersListUpdate() { public void onLiveEvent(Event event, RoomState roomState) { mRefreshUnreadCounter |= Event.EVENT_TYPE_MESSAGE.equals(event.getType()) || Event.EVENT_TYPE_RECEIPT.equals(event.getType()); - // TODO update to manage multisessions - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(VectorApp.getInstance().getApplicationContext()); - if (wm != null) { - wm.onLiveEvent(instance.getDefaultSession(), event); + WidgetManagerProvider wp = instance.mWidgetManagerProviders.get(instance.getDefaultSession().getMyUserId()); + if (wp != null) { + WidgetsManager wm = wp.getWidgetManager(VectorApp.getInstance().getApplicationContext()); + if (wm != null) { + wm.onLiveEvent(instance.getDefaultSession(), event); + } } } @@ -306,6 +310,23 @@ public List getSessions() { return sessions; } + @Nullable + public WidgetManagerProvider getWidgetManagerProvider(MXSession session) { + if (session == null) { + return null; + } + return mWidgetManagerProviders.get(session.getMyUserId()); + } + + @Nullable + public static WidgetsManager getWidgetManager(Context activity) { + if (Matrix.getInstance(activity) == null) return null; + MXSession session = Matrix.getInstance(activity).getDefaultSession(); + if (session == null) return null; + WidgetManagerProvider widgetManagerProvider = Matrix.getInstance(activity).getWidgetManagerProvider(session); + if (widgetManagerProvider == null) return null; + return widgetManagerProvider.getWidgetManager(activity); + } /** * Retrieve the default session if one exists. *

@@ -648,7 +669,9 @@ public synchronized void addSession(MXSession session) { * @return The session. */ public MXSession createSession(HomeServerConnectionConfig hsConfig) { - return createSession(mAppContext, hsConfig); + MXSession session = createSession(mAppContext, hsConfig); + mWidgetManagerProviders.put(session.getMyUserId(), new WidgetManagerProvider(session)); + return session; } /** diff --git a/vector/src/main/java/im/vector/VectorApp.java b/vector/src/main/java/im/vector/VectorApp.java index 187961c131..5e798fb0df 100755 --- a/vector/src/main/java/im/vector/VectorApp.java +++ b/vector/src/main/java/im/vector/VectorApp.java @@ -28,7 +28,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -197,11 +196,6 @@ public void onCreate() { Log.d(LOG_TAG, "onCreate"); super.onCreate(); - PreferencesManager.setIntegrationManagerUrls(this, - getString(R.string.integrations_ui_url), - getString(R.string.integrations_rest_url), - getString(R.string.integrations_jitsi_widget_url)); - mLifeCycleListener = new VectorLifeCycleObserver(); ProcessLifecycleOwner.get().getLifecycle().addObserver(mLifeCycleListener); diff --git a/vector/src/main/java/im/vector/activity/AbstractWidgetActivity.kt b/vector/src/main/java/im/vector/activity/AbstractWidgetActivity.kt index a4905a2152..d0d63ffc33 100755 --- a/vector/src/main/java/im/vector/activity/AbstractWidgetActivity.kt +++ b/vector/src/main/java/im/vector/activity/AbstractWidgetActivity.kt @@ -31,11 +31,11 @@ import im.vector.Matrix import im.vector.R import im.vector.activity.util.INTEGRATION_MANAGER_ACTIVITY_REQUEST_CODE import im.vector.activity.util.TERMS_REQUEST_CODE +import im.vector.fragments.roomwidgets.WebviewPermissionUtils import im.vector.types.JsonDict import im.vector.types.WidgetEventData import im.vector.util.AssetReader import im.vector.util.toJsonMap -import im.vector.widgets.WidgetManagerProvider import im.vector.widgets.WidgetsManager import org.jetbrains.anko.toast import org.matrix.androidsdk.MXSession @@ -88,7 +88,8 @@ abstract class AbstractWidgetActivity : VectorAppCompatActivity() { @CallSuper override fun initUiAndData() { - mSession = Matrix.getInstance(this).getSession(intent.getStringExtra(EXTRA_MATRIX_ID)) + val matrix = Matrix.getInstance(this) + mSession = matrix.getSession(intent.getStringExtra(EXTRA_MATRIX_ID)) if (null == mSession || !mSession!!.isAlive) { Log.e(LOG_TAG, "## onCreate() : invalid session") @@ -100,7 +101,7 @@ abstract class AbstractWidgetActivity : VectorAppCompatActivity() { mRoom = mSession!!.dataHandler.getRoom(intent.getStringExtra(EXTRA_ROOM_ID)) - widgetManager = WidgetManagerProvider.getWidgetManager(this) ?: run { + widgetManager = matrix.getWidgetManagerProvider(mSession)?.getWidgetManager(this) ?: run { finish() return } @@ -149,7 +150,7 @@ abstract class AbstractWidgetActivity : VectorAppCompatActivity() { } private fun presentTermsForServices(token: String) { - val wm = WidgetManagerProvider.getWidgetManager(this) + val wm = Matrix.getInstance(this).getWidgetManagerProvider(mSession)?.getWidgetManager(this)//WidgetManagerProvider.getWidgetManagerProvider(this) if (wm == null) { // should not happen finish() return @@ -194,7 +195,7 @@ abstract class AbstractWidgetActivity : VectorAppCompatActivity() { // Permission requests it.webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { - runOnUiThread { request.grant(request.resources) } + WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, this@AbstractWidgetActivity) } override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { diff --git a/vector/src/main/java/im/vector/activity/CommonActivityUtils.java b/vector/src/main/java/im/vector/activity/CommonActivityUtils.java index 600be07ba3..91f899042f 100755 --- a/vector/src/main/java/im/vector/activity/CommonActivityUtils.java +++ b/vector/src/main/java/im/vector/activity/CommonActivityUtils.java @@ -316,39 +316,34 @@ public static void logout(Activity activity) { private static boolean isRecoveringFromInvalidatedToken = false; public static void recoverInvalidatedToken() { + Log.e(LOG_TAG, "## recoverInvalidatedToken: Start Recover "); if (isRecoveringFromInvalidatedToken) { //ignore, we are doing it + Log.e(LOG_TAG, "## recoverInvalidatedToken: ignore, we are doing it"); return; } isRecoveringFromInvalidatedToken = true; Context context = VectorApp.getCurrentActivity() != null ? VectorApp.getCurrentActivity() : VectorApp.getInstance(); try { + + // Clear the credentials + Matrix.getInstance(context).getLoginStorage().clear() ; + + VectorApp.getInstance().getNotificationDrawerManager().clearAllEvents(); - EventStreamServiceX.Companion.onLogout(context); - // stopEventStream(context); + EventStreamServiceX.Companion.onApplicationStopped(context); BadgeProxy.INSTANCE.updateBadgeCount(context, 0); MXSession session = Matrix.getInstance(context).getDefaultSession(); - - // Publish to the server that we're now offline - MyPresenceManager.getInstance(context, session).advertiseOffline(); - MyPresenceManager.remove(session); - // clear the preferences PreferencesManager.clearPreferences(context); - // reset the FCM - Matrix.getInstance(context).getPushManager().resetFCMRegistration(); - // clear the preferences Matrix.getInstance(context).getPushManager().clearPreferences(); - // Clear the credentials - Matrix.getInstance(context).getLoginStorage().clear(); - // clear the tmp store list Matrix.getInstance(context).clearTmpStoresList(); @@ -358,20 +353,11 @@ public static void recoverInvalidatedToken() { MXMediaCache.clearThumbnailsCache(context); - Matrix.getInstance(context).clearSessions(context, true, new SimpleApiCallback() { - - @Override - public void onSuccess(Void info) { - - } - }); - session.clear(context); } catch (Exception e) { Log.e(LOG_TAG, "## recoverInvalidatedToken: Error while cleaning: ", e); } finally { - // go to login page - CommonActivityUtils.restartApp(context, true); isRecoveringFromInvalidatedToken = false; + CommonActivityUtils.restartApp(context, true); } } @@ -436,15 +422,12 @@ public void onSuccess(Void info) { if (goToLoginPage) { Activity activeActivity = VectorApp.getCurrentActivity(); + final Context activeContext = (null == activeActivity) ? VectorApp.getInstance().getApplicationContext() : activeActivity; + // go to login page - Intent intent = new Intent(activeActivity, LoginActivity.class); + Intent intent = new Intent(activeContext, LoginActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - - if (null != activeActivity) { - activeActivity.startActivity(intent); - } else { - context.startActivity(intent); - } + activeContext.startActivity(intent); } } }); diff --git a/vector/src/main/java/im/vector/activity/DialogUtils.kt b/vector/src/main/java/im/vector/activity/DialogUtils.kt new file mode 100644 index 0000000000..5c87db890d --- /dev/null +++ b/vector/src/main/java/im/vector/activity/DialogUtils.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 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.activity + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.R +import im.vector.extensions.showPassword + +object DialogUtils { + + fun promptPassword(context: Context, errorText: String? = null, defaultPwd: String? = null, + done: (String) -> Unit, + cancel: (() -> Unit)? = null) { + val view: ViewGroup = LayoutInflater.from(context).inflate(R.layout.dialog_confirm_password, null) as ViewGroup + + val showPassword: ImageView = view.findViewById(R.id.confirm_password_show_passwords) + val passwordTil: TextInputLayout = view.findViewById(R.id.confirm_password_til) + val passwordText: TextInputEditText = view.findViewById(R.id.password_label) + passwordText.setText(defaultPwd) + + var passwordShown = false + + showPassword.setOnClickListener { + passwordShown = !passwordShown + passwordText.showPassword(passwordShown) + showPassword.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + + passwordTil.error = errorText + + AlertDialog.Builder(context) + .setView(view) + .setPositiveButton(R.string._continue) { tv, _ -> + done(passwordText.text.toString()) + } + .apply { + if (cancel != null) { + setNegativeButton(R.string.cancel) { _, _ -> + cancel() + } + } + } + + .show() + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/activity/HandleBackParticipant.kt b/vector/src/main/java/im/vector/activity/HandleBackParticipant.kt new file mode 100644 index 0000000000..c976182bcb --- /dev/null +++ b/vector/src/main/java/im/vector/activity/HandleBackParticipant.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.activity + +/** + * A fragment should implement this interface if it wants to intercept backPressed events. + * Any activity extending VectorAppCompatActivity will propagate back pressed event to child + * fragment that implements it. + */ +interface HandleBackParticipant { + + /** + * Returns true, if the on back pressed event has been handled by this Fragment. + * Otherwise return false + */ + fun onBackPressed(): Boolean + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.kt b/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.kt index 1e0b32bee8..057dd99806 100755 --- a/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.kt +++ b/vector/src/main/java/im/vector/activity/IntegrationManagerActivity.kt @@ -259,7 +259,7 @@ class IntegrationManagerActivity : AbstractWidgetActivity() { Log.d(LOG_TAG, "Received request to get widget in room " + mRoom!!.roomId) - val widgets = widgetManager.getActiveWidgets(mSession, mRoom) + val widgets = WidgetsManager.getActiveWidgets(mSession, mRoom) val responseData = ArrayList>() for (widget in widgets) { diff --git a/vector/src/main/java/im/vector/activity/JitsiCallActivity.java b/vector/src/main/java/im/vector/activity/JitsiCallActivity.java index 7a9ec345e2..752e678ff1 100755 --- a/vector/src/main/java/im/vector/activity/JitsiCallActivity.java +++ b/vector/src/main/java/im/vector/activity/JitsiCallActivity.java @@ -27,15 +27,18 @@ import com.facebook.react.modules.core.PermissionListener; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jitsi.meet.sdk.JitsiMeetActivityDelegate; import org.jitsi.meet.sdk.JitsiMeetActivityInterface; import org.jitsi.meet.sdk.JitsiMeetConferenceOptions; +import org.jitsi.meet.sdk.JitsiMeetUserInfo; import org.jitsi.meet.sdk.JitsiMeetView; import org.jitsi.meet.sdk.JitsiMeetViewListener; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.core.Log; import org.matrix.androidsdk.data.Room; +import java.net.URL; import java.util.Map; import butterknife.BindView; @@ -64,7 +67,7 @@ public class JitsiCallActivity extends VectorAppCompatActivity implements JitsiM /** * Base server URL */ - private static final String JITSI_SERVER_URL = "https://jitsi.riot.im/"; + public static final String JITSI_SERVER_URL = "https://jitsi.riot.im/"; // the jitsi view private JitsiMeetView mJitsiView; @@ -109,10 +112,7 @@ public int getLayoutRes() { @Override @SuppressLint("NewApi") public void initUiAndData() { - if (WidgetManagerProvider.INSTANCE.getWidgetManager(this) == null) { - finish(); - return; - } + // Waiting View setWaitingView(findViewById(R.id.jitsi_progress_layout)); @@ -136,7 +136,6 @@ public void initUiAndData() { return; } - mRoom = mSession.getDataHandler().getRoom(mWidget.getRoomId()); if (null == mRoom) { Log.e(LOG_TAG, "## onCreate() : undefined room " + mWidget.getRoomId()); @@ -154,8 +153,22 @@ public void initUiAndData() { */ private void loadURL() { try { + JitsiMeetUserInfo userInfo = new JitsiMeetUserInfo(); + userInfo.setDisplayName(mSession.getMyUser().displayname); + try { + String avatarUrl = mSession.getMyUser().avatar_url; + if (avatarUrl != null) { + String downloadableUrl = mSession.getContentManager().getDownloadableUrl(avatarUrl, false); + if (downloadableUrl != null) { + userInfo.setAvatar(new URL(downloadableUrl)); + } + } + } catch (Exception e) { + //nop + } JitsiMeetConferenceOptions jitsiMeetConferenceOptions = new JitsiMeetConferenceOptions.Builder() .setVideoMuted(!mIsVideoCall) + .setUserInfo(userInfo) // Configure the title of the screen // TODO config.putString("callDisplayName", mRoom.getRoomDisplayName(this)); .setRoom(mCallUrl) @@ -237,7 +250,7 @@ public void onNewIntent(Intent intent) { protected void onStop() { super.onStop(); JitsiMeetActivityDelegate.onHostPause(this); - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(this); + WidgetsManager wm = getWidgetManager(); if (wm != null) { wm.removeListener(mWidgetListener); } @@ -253,12 +266,20 @@ protected void onResume() { super.onResume(); JitsiMeetActivityDelegate.onHostResume(this); - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(this); + WidgetsManager wm = getWidgetManager(); if (wm != null) { wm.addListener(mWidgetListener); } } + @Nullable + private WidgetsManager getWidgetManager() { + if (mSession == null) return null; + WidgetManagerProvider widgetManagerProvider = Matrix.getInstance(this).getWidgetManagerProvider(mSession); + if (widgetManagerProvider == null) return null; + return widgetManagerProvider.getWidgetManager(this); + } + @Override public void onBackPressed() { JitsiMeetActivityDelegate.onBackPressed(); diff --git a/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java b/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java index bd49371e9c..1d04b1969d 100644 --- a/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java +++ b/vector/src/main/java/im/vector/activity/PhoneNumberAdditionActivity.java @@ -267,7 +267,7 @@ private void addPhoneNumber(final Phonenumber.PhoneNumber phoneNumber) { @Override public void onSuccess(ThreePid pid) { hideWaitingView(); - Intent intent = PhoneNumberVerificationActivity.getIntent(PhoneNumberAdditionActivity.this, + Intent intent = PhoneNumberVerificationActivity.Companion.getIntent(PhoneNumberAdditionActivity.this, mSession.getCredentials().userId, pid); startActivityForResult(intent, REQUEST_VERIFICATION); } diff --git a/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java b/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java deleted file mode 100644 index d4f0ef6aed..0000000000 --- a/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2017 Vector Creations Ltd - * Copyright 2018 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.activity; - -import android.content.Context; -import android.content.Intent; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.inputmethod.EditorInfo; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; - -import org.matrix.androidsdk.MXSession; -import org.matrix.androidsdk.core.Log; -import org.matrix.androidsdk.core.callback.ApiCallback; -import org.matrix.androidsdk.core.model.MatrixError; -import org.matrix.androidsdk.rest.model.SuccessResult; -import org.matrix.androidsdk.rest.model.pid.ThreePid; - -import im.vector.Matrix; -import im.vector.R; - -public class PhoneNumberVerificationActivity extends VectorAppCompatActivity implements TextView.OnEditorActionListener, TextWatcher { - - private static final String LOG_TAG = PhoneNumberVerificationActivity.class.getSimpleName(); - - private static final String EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"; - private static final String EXTRA_PID = "EXTRA_PID"; - - private TextInputEditText mPhoneNumberCode; - private TextInputLayout mPhoneNumberCodeLayout; - - private MXSession mSession; - private ThreePid mThreePid; - - // True when a phone number token is submitted - // Used to prevent user to submit several times in a row - private boolean mIsSubmittingToken; - - /* - * ********************************************************************************************* - * Static methods - * ********************************************************************************************* - */ - - public static Intent getIntent(final Context context, final String sessionId, final ThreePid pid) { - final Intent intent = new Intent(context, PhoneNumberVerificationActivity.class); - intent.putExtra(EXTRA_MATRIX_ID, sessionId); - intent.putExtra(EXTRA_PID, pid); - return intent; - } - - /* - * ********************************************************************************************* - * Activity lifecycle - * ********************************************************************************************* - */ - - @Override - public int getLayoutRes() { - return R.layout.activity_phone_number_verification; - } - - @Override - public int getTitleRes() { - return R.string.settings_phone_number_verification; - } - - @Override - public void initUiAndData() { - configureToolbar(); - - mPhoneNumberCode = findViewById(R.id.phone_number_code_value); - mPhoneNumberCodeLayout = findViewById(R.id.phone_number_code); - setWaitingView(findViewById(R.id.loading_view)); - - final Intent intent = getIntent(); - mSession = Matrix.getInstance(this).getSession(intent.getStringExtra(EXTRA_MATRIX_ID)); - - if ((null == mSession) || !mSession.isAlive()) { - finish(); - return; - } - - mThreePid = intent.getParcelableExtra(EXTRA_PID); - - mPhoneNumberCode.addTextChangedListener(this); - mPhoneNumberCode.setOnEditorActionListener(this); - } - - @Override - protected void onResume() { - super.onResume(); - mIsSubmittingToken = false; - } - - @Override - public int getMenuRes() { - return R.menu.menu_phone_number_verification; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_verify_phone_number: - submitCode(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - /* - * ********************************************************************************************* - * Utils - * ********************************************************************************************* - */ - - /** - * Submit code (token) to attach phone number to account - */ - private void submitCode() { - if (!mIsSubmittingToken) { - mIsSubmittingToken = true; - if (TextUtils.isEmpty(mPhoneNumberCode.getText())) { - mPhoneNumberCodeLayout.setErrorEnabled(true); - mPhoneNumberCodeLayout.setError(getString(R.string.settings_phone_number_verification_error_empty_code)); - } else { - showWaitingView(); - mSession.getIdentityServerManager().submitValidationToken(mThreePid, mPhoneNumberCode.getText().toString(), - new ApiCallback() { - @Override - public void onSuccess(SuccessResult result) { - if (result.success) { - // the validation of mail ownership succeed, just resume the registration flow - // next step: just register - Log.e(LOG_TAG, "## submitPhoneNumberValidationToken(): onSuccess() - registerAfterEmailValidations() started"); - registerAfterPhoneNumberValidation(mThreePid); - } else { - Log.e(LOG_TAG, "## submitPhoneNumberValidationToken(): onSuccess() - failed (success=false)"); - onSubmitCodeError(getString(R.string.settings_phone_number_verification_error)); - } - } - - @Override - public void onNetworkError(Exception e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - - @Override - public void onMatrixError(MatrixError e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - - @Override - public void onUnexpectedError(Exception e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - }); - } - - } - } - - private void registerAfterPhoneNumberValidation(final ThreePid pid) { - mSession.getIdentityServerManager().finalizeAddSessionForEmail(pid, new ApiCallback() { - @Override - public void onSuccess(Void info) { - Intent intent = new Intent(); - setResult(RESULT_OK, intent); - finish(); - } - - @Override - public void onNetworkError(Exception e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - - @Override - public void onMatrixError(MatrixError e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - - @Override - public void onUnexpectedError(Exception e) { - onSubmitCodeError(e.getLocalizedMessage()); - } - }); - } - - private void onSubmitCodeError(final String errorMessage) { - mIsSubmittingToken = false; - hideWaitingView(); - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show(); - } - - /* - * ********************************************************************************************* - * Listeners - * ********************************************************************************************* - */ - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE && !isFinishing()) { - submitCode(); - return true; - } - return false; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (mPhoneNumberCodeLayout.getError() != null) { - mPhoneNumberCodeLayout.setError(null); - mPhoneNumberCodeLayout.setErrorEnabled(false); - } - } -} diff --git a/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.kt b/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.kt new file mode 100644 index 0000000000..c08eb3e81b --- /dev/null +++ b/vector/src/main/java/im/vector/activity/PhoneNumberVerificationActivity.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 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.activity + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.MenuItem +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.Matrix +import im.vector.R +import org.matrix.androidsdk.MXSession +import org.matrix.androidsdk.core.JsonUtils +import org.matrix.androidsdk.core.Log +import org.matrix.androidsdk.core.callback.ApiCallback +import org.matrix.androidsdk.core.model.MatrixError +import org.matrix.androidsdk.rest.client.LoginRestClient +import org.matrix.androidsdk.rest.model.SuccessResult +import org.matrix.androidsdk.rest.model.login.AuthParams +import org.matrix.androidsdk.rest.model.login.AuthParamsLoginPassword +import org.matrix.androidsdk.rest.model.pid.ThreePid + +class PhoneNumberVerificationActivity : VectorAppCompatActivity(), TextView.OnEditorActionListener, TextWatcher { + + private var mPhoneNumberCode: TextInputEditText? = null + private var mPhoneNumberCodeLayout: TextInputLayout? = null + + private var mSession: MXSession? = null + private var mThreePid: ThreePid? = null + + // True when a phone number token is submitted + // Used to prevent user to submit several times in a row + private var mIsSubmittingToken: Boolean = false + + /* + * ********************************************************************************************* + * Activity lifecycle + * ********************************************************************************************* + */ + + override fun getLayoutRes(): Int { + return R.layout.activity_phone_number_verification + } + + override fun getTitleRes(): Int { + return R.string.settings_phone_number_verification + } + + override fun initUiAndData() { + configureToolbar() + + mPhoneNumberCode = findViewById(R.id.phone_number_code_value) + mPhoneNumberCodeLayout = findViewById(R.id.phone_number_code) + waitingView = findViewById(R.id.loading_view) + + val intent = intent + mSession = Matrix.getInstance(this)!!.getSession(intent.getStringExtra(EXTRA_MATRIX_ID)) + + if (null == mSession || !mSession!!.isAlive) { + finish() + return + } + + mThreePid = intent.getParcelableExtra(EXTRA_PID) + + mPhoneNumberCode!!.addTextChangedListener(this) + mPhoneNumberCode!!.setOnEditorActionListener(this) + } + + override fun onResume() { + super.onResume() + mIsSubmittingToken = false + } + + override fun getMenuRes(): Int { + return R.menu.menu_phone_number_verification + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_verify_phone_number -> { + submitCode() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + /* + * ********************************************************************************************* + * Utils + * ********************************************************************************************* + */ + + /** + * Submit code (token) to attach phone number to account + */ + private fun submitCode() { + if (!mIsSubmittingToken) { + mIsSubmittingToken = true + if (TextUtils.isEmpty(mPhoneNumberCode!!.text)) { + mPhoneNumberCodeLayout!!.isErrorEnabled = true + mPhoneNumberCodeLayout!!.error = getString(R.string.settings_phone_number_verification_error_empty_code) + } else { + showWaitingView() + mSession!!.identityServerManager.submitValidationToken(mThreePid!!, mPhoneNumberCode!!.text!!.toString(), + object : ApiCallback { + override fun onSuccess(result: SuccessResult) { + if (result.success) { + // the validation of mail ownership succeed, just resume the registration flow + // next step: just register + Log.e(LOG_TAG, "## submitPhoneNumberValidationToken(): onSuccess() - registerAfterEmailValidations() started") + registerAfterPhoneNumberValidation(mThreePid, null) + } else { + Log.e(LOG_TAG, "## submitPhoneNumberValidationToken(): onSuccess() - failed (success=false)") + onSubmitCodeError(getString(R.string.settings_phone_number_verification_error)) + } + } + + override fun onNetworkError(e: Exception) { + onSubmitCodeError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onSubmitCodeError(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onSubmitCodeError(e.localizedMessage) + } + }) + } + + } + } + + private fun registerAfterPhoneNumberValidation(pid: ThreePid?, auth: AuthParams?) { + mSession!!.identityServerManager.finalize3pidAddSession(pid!!, auth, object : ApiCallback { + override fun onSuccess(info: Void?) { + val intent = Intent() + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onNetworkError(e: Exception) { + onSubmitCodeError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (e.mStatus == 401 && e.mErrorBodyAsString.isNullOrBlank().not()) { + val flow = JsonUtils.toRegistrationFlowResponse(e.mErrorBodyAsString) + if (flow != null) { + val supportsLoginPassword = flow.flows.any { it.stages == listOf(LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD) } + if (supportsLoginPassword) { + //we prompt for it + + val invalidPassError = getString(R.string.login_error_forbidden) + .takeIf { e.errcode == MatrixError.FORBIDDEN } + + DialogUtils.promptPassword(this@PhoneNumberVerificationActivity, + invalidPassError, + (auth as? AuthParamsLoginPassword)?.let { auth.password }, + { password -> + val authParams = AuthParamsLoginPassword().apply { + this.user = mSession?.myUserId + this.password = password + } + registerAfterPhoneNumberValidation(pid, authParams) + }, + { + onSubmitCodeError(getString(R.string.settings_add_3pid_authentication_needed)) + }) + } else { + //you can only do that on mobile + AlertDialog.Builder(this@PhoneNumberVerificationActivity) + .setTitle(R.string.dialog_title_error) + .setMessage(R.string.settings_add_3pid_flow_not_supported) + .setPositiveButton(R.string._continue) { _, _ -> + hideWaitingView() + } + .show() + + } + + } else { + onSubmitCodeError(e.localizedMessage) + } + } else { + onSubmitCodeError(e.localizedMessage) + } + + } + + override fun onUnexpectedError(e: Exception) { + onSubmitCodeError(e.localizedMessage) + } + }) + } + + private fun onSubmitCodeError(errorMessage: String) { + mIsSubmittingToken = false + hideWaitingView() + Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() + } + + /* + * ********************************************************************************************* + * Listeners + * ********************************************************************************************* + */ + + override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE && !isFinishing) { + submitCode() + return true + } + return false + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable) { + if (mPhoneNumberCodeLayout!!.error != null) { + mPhoneNumberCodeLayout!!.error = null + mPhoneNumberCodeLayout!!.isErrorEnabled = false + } + } + + companion object { + + private val LOG_TAG = PhoneNumberVerificationActivity::class.java.simpleName + + private val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" + private val EXTRA_PID = "EXTRA_PID" + + /* + * ********************************************************************************************* + * Static methods + * ********************************************************************************************* + */ + + fun getIntent(context: Context, sessionId: String, pid: ThreePid): Intent { + val intent = Intent(context, PhoneNumberVerificationActivity::class.java) + intent.putExtra(EXTRA_MATRIX_ID, sessionId) + intent.putExtra(EXTRA_PID, pid) + return intent + } + } +} diff --git a/vector/src/main/java/im/vector/activity/StickerPickerActivity.kt b/vector/src/main/java/im/vector/activity/StickerPickerActivity.kt index a1ed6c3237..f83c623198 100755 --- a/vector/src/main/java/im/vector/activity/StickerPickerActivity.kt +++ b/vector/src/main/java/im/vector/activity/StickerPickerActivity.kt @@ -55,7 +55,7 @@ class StickerPickerActivity : AbstractWidgetActivity() { } override fun canScalarTokenBeProvided(): Boolean { - return widgetManager.isScalarUrl(this, mWidgetUrl) + return widgetManager.isScalarUrl( mWidgetUrl) } /** diff --git a/vector/src/main/java/im/vector/activity/VectorAppCompatActivity.kt b/vector/src/main/java/im/vector/activity/VectorAppCompatActivity.kt index 5c867ae13b..c6fcdae99f 100755 --- a/vector/src/main/java/im/vector/activity/VectorAppCompatActivity.kt +++ b/vector/src/main/java/im/vector/activity/VectorAppCompatActivity.kt @@ -24,17 +24,20 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.annotation.* -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder +import com.airbnb.mvrx.BaseMvRxActivity import im.vector.BuildConfig import im.vector.R import im.vector.VectorApp import im.vector.activity.interfaces.Restorable import im.vector.dialogs.ConsentNotGivenHelper +import im.vector.fragments.VectorBaseFragment +import im.vector.fragments.VectorBaseMvRxFragment import im.vector.receiver.DebugReceiver import im.vector.ui.themes.ActivityOtherThemes import im.vector.ui.themes.ThemeUtils @@ -45,7 +48,7 @@ import org.matrix.androidsdk.core.Log /** * Parent class for all Activities in Vector application */ -abstract class VectorAppCompatActivity : AppCompatActivity() { +abstract class VectorAppCompatActivity : BaseMvRxActivity() { private var LOG_TAG = VectorAppCompatActivity::class.java.simpleName @@ -151,6 +154,27 @@ abstract class VectorAppCompatActivity : AppCompatActivity() { } } + override fun onBackPressed() { + val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) + if (!handled) { + super.onBackPressed() + } + } + + private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { + val reverseOrder = fm.fragments.filter { it is VectorBaseFragment || it is VectorBaseMvRxFragment }.reversed() + for (f in reverseOrder) { + val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) + if (handledByChildFragments) { + return true + } + if (f is HandleBackParticipant && f.onBackPressed()) { + return true + } + } + return false + } + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) diff --git a/vector/src/main/java/im/vector/activity/VectorRoomActivity.java b/vector/src/main/java/im/vector/activity/VectorRoomActivity.java index a76802317a..c12ca7b98a 100755 --- a/vector/src/main/java/im/vector/activity/VectorRoomActivity.java +++ b/vector/src/main/java/im/vector/activity/VectorRoomActivity.java @@ -118,6 +118,7 @@ import im.vector.fragments.VectorMessageListFragment; import im.vector.fragments.VectorReadReceiptsDialogFragment; import im.vector.fragments.VectorUnknownDevicesFragment; +import im.vector.fragments.roomwidgets.RoomWidgetPermissionBottomSheet; import im.vector.listeners.IMessagesAdapterActionsListener; import im.vector.ui.themes.ThemeUtils; import im.vector.util.CallsManager; @@ -128,6 +129,7 @@ import im.vector.util.ReadMarkerManager; import im.vector.util.RoomUtils; import im.vector.util.SlashCommandsParser; +import im.vector.util.UrlUtilKt; import im.vector.util.VectorMarkdownParser; import im.vector.util.VectorRoomMediasSender; import im.vector.util.VectorUtils; @@ -137,8 +139,8 @@ import im.vector.view.VectorOngoingConferenceCallView; import im.vector.view.VectorPendingCallView; import im.vector.widgets.Widget; -import im.vector.widgets.WidgetManagerProvider; import im.vector.widgets.WidgetsManager; +import kotlin.Unit; /** * Displays a single room with messages. @@ -864,7 +866,7 @@ public void onCloseWidgetClick(final Widget widget) { .setPositiveButton(R.string.remove, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(VectorRoomActivity.this); + WidgetsManager wm = Matrix.getWidgetManager(VectorRoomActivity.this); if (wm != null) { showWaitingView(); @@ -925,7 +927,7 @@ public void onClick(final List widgets) { } new AlertDialog.Builder(VectorRoomActivity.this) - .setSingleChoiceItems(widgetNames.toArray(CharSequences), 0, new DialogInterface.OnClickListener() { + .setItems(widgetNames.toArray(CharSequences), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface d, int n) { d.cancel(); @@ -969,7 +971,7 @@ public void onVideoCallClick(Widget widget) { @Override public void onCloseWidgetClick(Widget widget) { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(VectorRoomActivity.this); + WidgetsManager wm = Matrix.getWidgetManager(VectorRoomActivity.this); if (wm != null) { showWaitingView(); @@ -1493,7 +1495,7 @@ public boolean onPrepareOptionsMenu(Menu menu) { return true; } - boolean hasIntegrationManager = WidgetManagerProvider.INSTANCE.getWidgetManager(this) != null; + boolean hasIntegrationManager = Matrix.getWidgetManager(this) != null; // the menu is only displayed when the current activity does not display a timeline search if (TextUtils.isEmpty(mEventId) && (null == sRoomPreviewData)) { @@ -1637,7 +1639,7 @@ private void openIntegrationManagerActivity(@Nullable String screenId) { return; } - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(this); + WidgetsManager wm = Matrix.getWidgetManager(this); if (wm == null) { //Should not happen this action is not activated if no wm return; @@ -1766,10 +1768,31 @@ private void launchJitsiActivity(Widget widget, boolean aIsVideoCall) { .setPositiveButton(R.string.ok, null) .show(); } else { - final Intent intent = new Intent(this, JitsiCallActivity.class); - intent.putExtra(JitsiCallActivity.EXTRA_WIDGET_ID, widget); - intent.putExtra(JitsiCallActivity.EXTRA_ENABLE_VIDEO, aIsVideoCall); - startActivity(intent); + //Here check native widget perm + + String domain = UrlUtilKt.extractDomain(JitsiCallActivity.JITSI_SERVER_URL); + if (domain == null) return; //display a toast? + boolean isAllowed = mSession.getIntegrationManager().isNativeWidgetAllowed("jitsi", domain); + if (isAllowed) { + final Intent intent = new Intent(this, JitsiCallActivity.class); + intent.putExtra(JitsiCallActivity.EXTRA_WIDGET_ID, widget); + intent.putExtra(JitsiCallActivity.EXTRA_ENABLE_VIDEO, aIsVideoCall); + startActivity(intent); + } else { + //we need to prompt for permissions + RoomWidgetPermissionBottomSheet bs = RoomWidgetPermissionBottomSheet.Companion + .newInstance(mSession.getMyUserId(), widget); + bs.setOnFinish((accepted) -> { + if (accepted) { + final Intent intent = new Intent(this, JitsiCallActivity.class); + intent.putExtra(JitsiCallActivity.EXTRA_WIDGET_ID, widget); + intent.putExtra(JitsiCallActivity.EXTRA_ENABLE_VIDEO, aIsVideoCall); + startActivity(intent); + } + return Unit.INSTANCE; + }); + bs.show(getSupportFragmentManager(), "JitsiPerm"); + } } } @@ -1779,7 +1802,7 @@ private void launchJitsiActivity(Widget widget, boolean aIsVideoCall) { * @param aIsVideoCall true if the call is a video one */ private void startJitsiCall(final boolean aIsVideoCall) { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(this); + WidgetsManager wm = Matrix.getWidgetManager(this); if (wm != null) { enableActionBarHeader(HIDE_ACTION_BAR_HEADER); showWaitingView(); @@ -3871,7 +3894,7 @@ private void chooseMediaSource(boolean useNativeCamera, boolean isVoiceFeatureEn } // Send sticker - if (WidgetManagerProvider.INSTANCE.getWidgetManager(this) != null) { + if (Matrix.getWidgetManager(this) != null) { items.add(DialogListItem.SendSticker.INSTANCE); } @@ -4009,13 +4032,13 @@ void onStartCallClick() { .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(R.string.ok, null) .show(); - } else if (WidgetManagerProvider.INSTANCE.getWidgetManager(this) == null) { - // display the dialog with the info text - new AlertDialog.Builder(this) - .setMessage(R.string.integration_manager_not_configured) - .setIcon(android.R.drawable.ic_dialog_alert) - .setPositiveButton(R.string.ok, null) - .show(); +// } else if (Matrix.getWidgetManager(this) == null) { +// // display the dialog with the info text +// new AlertDialog.Builder(this) +// .setMessage(R.string.integration_manager_not_configured) +// .setIcon(android.R.drawable.ic_dialog_alert) +// .setPositiveButton(R.string.ok, null) +// .show(); } else if (isUserAllowedToStartConfCall()) { if (mRoom.getNumberOfMembers() > 2) { new AlertDialog.Builder(this) @@ -4026,7 +4049,16 @@ void onStartCallClick() { @Override public void onClick(DialogInterface dialog, int which) { if (PreferencesManager.useJitsiConfCall(VectorRoomActivity.this)) { - startJitsiCall(true); + if (Matrix.getWidgetManager(VectorRoomActivity.this) == null) { + // display the dialog with the info text + new AlertDialog.Builder(VectorRoomActivity.this) + .setMessage(R.string.integration_manager_not_configured) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.ok, null) + .show(); + } else { + startJitsiCall(true); + } } else { displayVideoCallIpDialog(); } diff --git a/vector/src/main/java/im/vector/activity/WidgetActivity.kt b/vector/src/main/java/im/vector/activity/WidgetActivity.kt index a74d8e537f..388c74d1c4 100755 --- a/vector/src/main/java/im/vector/activity/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/activity/WidgetActivity.kt @@ -20,71 +20,22 @@ package im.vector.activity import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.text.TextUtils -import android.view.View -import android.view.ViewGroup -import android.webkit.* -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import butterknife.BindView -import butterknife.OnClick -import im.vector.Matrix +import android.graphics.Color +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import com.airbnb.mvrx.viewModel import im.vector.R +import im.vector.fragments.roomwidgets.* +import im.vector.ui.themes.ThemeUtils import im.vector.widgets.Widget -import im.vector.widgets.WidgetManagerProvider -import im.vector.widgets.WidgetsManager -import org.jetbrains.anko.toast -import org.matrix.androidsdk.MXSession -import org.matrix.androidsdk.core.Log -import org.matrix.androidsdk.core.callback.ApiCallback -import org.matrix.androidsdk.core.model.MatrixError -import org.matrix.androidsdk.data.Room -import javax.net.ssl.HttpsURLConnection /* * This class displays a widget */ class WidgetActivity : VectorAppCompatActivity() { - // the linked widget - private var mWidget: Widget? = null - - // the session - private var mSession: MXSession? = null - - // the room - private var mRoom: Room? = null - - @BindView(R.id.widget_close_icon) - lateinit var mCloseWidgetIcon: View - - @BindView(R.id.widget_web_view) - lateinit var mWidgetWebView: WebView - - @BindView(R.id.widget_title) - lateinit var mWidgetTypeTextView: TextView - - private var mIsRefreshingToken = false - private var mTokenAlreadyRefreshed = false - private var mHistoryAlreadyCleared = false - - private lateinit var widgetsManager: WidgetsManager - /** - * Widget events listener - */ - private val mWidgetListener = WidgetsManager.onWidgetUpdateListener { widget -> - if (TextUtils.equals(widget.widgetId, mWidget!!.widgetId)) { - if (!widget.isActive) { - finish() - } - } - } + val viewModel: RoomWidgetViewModel by viewModel() /* ========================================================================================== * LIFE CYCLE @@ -92,266 +43,66 @@ class WidgetActivity : VectorAppCompatActivity() { override fun getLayoutRes() = R.layout.activity_widget - @SuppressLint("NewApi") - override fun initUiAndData() { - waitingView = findViewById(R.id.widget_progress_layout) - - mWidget = intent.getSerializableExtra(EXTRA_WIDGET_ID) as Widget - - if (null == mWidget || null == mWidget!!.url) { - Log.e(LOG_TAG, "## onCreate() : invalid widget") - finish() - return - } - - widgetsManager = WidgetManagerProvider.getWidgetManager(this) ?: run { - Log.e(LOG_TAG, "## onCreate() : No widget manager ") - finish() - return - } - - mSession = Matrix.getMXSession(this, mWidget!!.sessionId) - - if (null == mSession) { - Log.e(LOG_TAG, "## onCreate() : invalid session") - finish() - return - } - - mRoom = mSession!!.dataHandler.getRoom(mWidget!!.roomId) - - if (null == mRoom) { - Log.e(LOG_TAG, "## onCreate() : invalid room") - finish() - return - } - - mWidgetTypeTextView.text = mWidget!!.humanName - - configureWebView() - - loadUrl() - } - - override fun onDestroy() { - mWidgetWebView.let { - (it.parent as ViewGroup).removeView(it) - it.removeAllViews() - it.destroy() - } - - super.onDestroy() - } - - override fun onResume() { - super.onResume() - - widgetsManager.addListener(mWidgetListener) - - mWidgetWebView.let { - it.resumeTimers() - it.onResume() - } - - refreshStatusBar() - } - - override fun onPause() { - super.onPause() - - mWidgetWebView.let { - it.pauseTimers() - it.onPause() - } - - widgetsManager.removeListener(mWidgetListener) - } - - /* ========================================================================================== - * UI EVENTS - * ========================================================================================== */ - - @OnClick(R.id.widget_close_icon) - internal fun onCloseClick() { - AlertDialog.Builder(this) - .setMessage(R.string.widget_delete_message_confirmation) - .setPositiveButton(R.string.remove) { _, _ -> - showWaitingView() - widgetsManager.closeWidget(mSession, mRoom, mWidget!!.widgetId, object : ApiCallback { - override fun onSuccess(info: Void?) { - hideWaitingView() - finish() - } - - private fun onError(errorMessage: String) { - hideWaitingView() - toast(errorMessage) - } - - override fun onNetworkError(e: Exception) { - onError(e.localizedMessage) - } + override fun getMenuRes() = R.menu.menu_room_widget - override fun onMatrixError(e: MatrixError) { - onError(e.localizedMessage) - } + override fun getTitleRes() = R.string.room_widget_activity_title - override fun onUnexpectedError(e: Exception) { - onError(e.localizedMessage) - } - }) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - @OnClick(R.id.widget_back_icon) - internal fun onBackClicked() { - if (mWidgetWebView.canGoBack()) { - mWidgetWebView.goBack() - } else { - finish() - } - } - - /* ========================================================================================== - * PRIVATE - * ========================================================================================== */ - - /** - * Refresh the status bar - */ - private fun refreshStatusBar() { - val canCloseWidget = null == widgetsManager.checkWidgetPermission(mSession, mRoom) - - // close widget button - mCloseWidgetIcon.isInvisible = !canCloseWidget - } - - /** - * Load the widget call - */ @SuppressLint("NewApi") - private fun configureWebView() { - mWidgetWebView.let { - // xml value seems ignored - it.setBackgroundColor(0) - - // clear caches - it.clearHistory() - it.clearFormData() - it.clearCache(true) - - it.settings.let { - // does not cache the data - it.cacheMode = WebSettings.LOAD_NO_CACHE - - // Enable Javascript - it.javaScriptEnabled = true - - // Use WideViewport and Zoom out if there is no viewport defined - it.useWideViewPort = true - it.loadWithOverviewMode = true - - // Enable pinch to zoom without the zoom buttons - it.builtInZoomControls = true - - // Allow use of Local Storage - it.domStorageEnabled = true - - it.allowFileAccessFromFileURLs = true - it.allowUniversalAccessFromFileURLs = true - - it.displayZoomControls = false - } + override fun initUiAndData() { + configureToolbar() - // Permission requests - it.webChromeClient = object : WebChromeClient() { - override fun onPermissionRequest(request: PermissionRequest) { - Handler(Looper.getMainLooper()).post { request.grant(request.resources) } - } - } + supportActionBar?.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_material_leave)?.let { + ThemeUtils.tintDrawableWithColor(it, Color.WHITE) + }) - it.webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - showWaitingView() + viewModel.selectSubscribe(this, RoomWidgetViewModelState::status) { ws -> + when (ws) { + WidgetState.UNKNOWN -> { } - - override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) { - // In case of 403, try to refresh the scalar token - if (errorResponse?.statusCode == HttpsURLConnection.HTTP_FORBIDDEN - && !mTokenAlreadyRefreshed - && widgetsManager.isScalarUrl(this@WidgetActivity, mWidget!!.url)) { - mTokenAlreadyRefreshed = true - mIsRefreshingToken = true - - // Hide the webview because it's displaying an error message we try to fix by refreshing the token - mWidgetWebView.isVisible = false - - widgetsManager.clearScalarToken(this@WidgetActivity, mSession) - loadUrl() + WidgetState.WIDGET_NOT_ALLOWED -> { + val dFrag = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG_PERMISSION) as? RoomWidgetPermissionBottomSheet + if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { + //already there + } else { + RoomWidgetPermissionBottomSheet + .newInstance(viewModel.session!!.myUserId, viewModel.widget).apply { + onFinish = { accepted -> + if (!accepted) finish() + } + } + .show(supportFragmentManager, FRAGMENT_TAG_PERMISSION) } } - - override fun onPageFinished(view: WebView?, url: String?) { - // Check that the Activity is still alive - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed) { - return - } - - if (mIsRefreshingToken) { - // We are waiting for a scalar token refresh - return - } - - hideWaitingView() - - // Ensure the webview is visible, it may have been hidden during token refresh - mWidgetWebView.isVisible = true - - if (mTokenAlreadyRefreshed && !mHistoryAlreadyCleared) { - // Also clear WebView history, for the scenario when the scalar token was invalid, to avoid loading again the url with the invalid token - // It has to be done when page has finished to be loaded - mHistoryAlreadyCleared = true - mWidgetWebView.clearHistory() + WidgetState.WIDGET_ALLOWED -> { + //mount the webview fragment if needed + if (supportFragmentManager.findFragmentByTag(FRAGMENT_TAG_WEBVIEW) == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, RoomWidgetFragment(), FRAGMENT_TAG_WEBVIEW) + .commit() } - } } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val cookieManager = android.webkit.CookieManager.getInstance() - cookieManager.setAcceptThirdPartyCookies(mWidgetWebView, true) - } } - } - - private fun loadUrl() { - showWaitingView() - widgetsManager.getFormattedWidgetUrl(this, mWidget!!, object : ApiCallback { - override fun onSuccess(url: String) { - mIsRefreshingToken = false - - hideWaitingView() - mWidgetWebView.loadUrl(url) - } - private fun onError(errorMessage: String) { - hideWaitingView() - toast(errorMessage) - finish() - } + viewModel.selectSubscribe(this, RoomWidgetViewModelState::widgetName) { name -> + supportActionBar?.title = name + } - override fun onNetworkError(e: Exception) { - onError(e.localizedMessage) - } + viewModel.selectSubscribe(this, RoomWidgetViewModelState::canManageWidgets) { + invalidateOptionsMenu() + } - override fun onMatrixError(e: MatrixError) { - onError(e.localizedMessage) + viewModel.navigateEvent.observe(this, Observer { uxStateEvent -> + when (uxStateEvent?.getContentIfNotHandled()) { + RoomWidgetViewModel.NAVIGATE_FINISH -> { + finish() + } } + }) - override fun onUnexpectedError(e: Exception) { - onError(e.localizedMessage) + viewModel.toastMessageEvent.observe(this, Observer { + it?.getContentIfNotHandled()?.let { + Toast.makeText(this, it, Toast.LENGTH_LONG).show() } }) } @@ -361,12 +112,13 @@ class WidgetActivity : VectorAppCompatActivity() { * ========================================================================================== */ companion object { - private val LOG_TAG = WidgetActivity::class.java.simpleName + private const val FRAGMENT_TAG_PERMISSION = "FRAGMENT_TAG_PERMISSION" + private const val FRAGMENT_TAG_WEBVIEW = "WebView" /** * The linked widget */ - private const val EXTRA_WIDGET_ID = "EXTRA_WIDGET_ID" + const val EXTRA_WIDGET_ID = "EXTRA_WIDGET_ID" fun getIntent(context: Context, widget: Widget): Intent { return Intent(context, WidgetActivity::class.java) diff --git a/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.kt index aff7df47d1..37c96aa7f8 100755 --- a/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/fragments/VectorSettingsPreferencesFragment.kt @@ -44,6 +44,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity import androidx.preference.* import com.bumptech.glide.Glide import com.google.android.material.textfield.TextInputEditText @@ -60,11 +61,7 @@ import im.vector.dialogs.ExportKeysDialog import im.vector.extensions.getFingerprintHumanReadable import im.vector.extensions.showPassword import im.vector.extensions.withArgs -import im.vector.fragments.troubleshoot.NotificationTroubleshootTestManager -import im.vector.preference.ProgressBarPreference -import im.vector.preference.UserAvatarPreference -import im.vector.preference.VectorGroupPreference -import im.vector.preference.VectorPreference +import im.vector.preference.* import im.vector.settings.FontScale import im.vector.settings.VectorLocale import im.vector.ui.themes.ThemeUtils @@ -74,6 +71,7 @@ import org.jetbrains.anko.toast import org.matrix.androidsdk.MXSession import org.matrix.androidsdk.call.MXCallsManager import org.matrix.androidsdk.core.BingRulesManager +import org.matrix.androidsdk.core.JsonUtils import org.matrix.androidsdk.core.Log import org.matrix.androidsdk.core.ResourceUtils import org.matrix.androidsdk.core.callback.ApiCallback @@ -88,10 +86,14 @@ import org.matrix.androidsdk.data.MyUser import org.matrix.androidsdk.data.Pusher import org.matrix.androidsdk.data.RoomMediaMessage import org.matrix.androidsdk.db.MXMediaCache +import org.matrix.androidsdk.features.identityserver.IdentityServerManager +import org.matrix.androidsdk.features.integrationmanager.IntegrationManager import org.matrix.androidsdk.listeners.MXEventListener import org.matrix.androidsdk.listeners.MXMediaUploadListener +import org.matrix.androidsdk.rest.client.LoginRestClient import org.matrix.androidsdk.rest.model.bingrules.BingRule import org.matrix.androidsdk.rest.model.group.Group +import org.matrix.androidsdk.rest.model.login.AuthParamsLoginPassword import org.matrix.androidsdk.rest.model.pid.ThirdPartyIdentifier import org.matrix.androidsdk.rest.model.pid.ThreePid import org.matrix.androidsdk.rest.model.sync.AccountDataElement @@ -126,7 +128,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref override fun onAccountDataUpdated(accountDataElement: AccountDataElement) { if (accountDataElement.type == AccountDataElement.ACCOUNT_DATA_TYPE_IDENTITY_SERVER) { - (findPreference(PreferencesManager.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY) as EditTextPreference).let { + (findPreference(PreferencesManager.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY) as VectorPreference).let { updateIdentityServerPref() } } @@ -152,6 +154,12 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref // current publicised group list private var mPublicisedGroups: MutableSet? = null + private var integrationManagerManagerListener = object : IntegrationManager.IntegrationManagerManagerListener { + override fun onIntegrationManagerChange(managerConfig: IntegrationManager) { + refreshIntegrationManagerSettings() + } + } + /* ========================================================================================== * Preferences * ========================================================================================== */ @@ -621,8 +629,33 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref // identity server updateIdentityServerPref() - findPreference(PreferencesManager.SETTINGS_INTEGRATION_MANAGER_UI_URL) - .summary = PreferencesManager.getIntegrationManagerUiUrl(context) + + (findPreference(PreferencesManager.SETTINGS_INTEGRATION_ALLOW) as? VectorSwitchPreference)?.let { + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + + //Disable it while updating the state, will be re-enabled by the account data listener. + it.isEnabled = false + mSession.integrationManager.enableIntegrationManagerUsage(newValue as Boolean, object : ApiCallback { + override fun onSuccess(info: Void?) { + refreshIntegrationManagerSettings() + } + + override fun onUnexpectedError(e: java.lang.Exception?) { + refreshIntegrationManagerSettings() + } + + override fun onNetworkError(e: java.lang.Exception?) { + refreshIntegrationManagerSettings() + } + + override fun onMatrixError(e: MatrixError?) { + refreshIntegrationManagerSettings() + } + + }) + true + } + } // Analytics @@ -789,7 +822,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref } else { MXCallsManager.defaultStunServerUri = null } - PreferencesManager.setUseDefaultTurnServer(activity,mUseDefaultStunPreference.isChecked) + PreferencesManager.setUseDefaultTurnServer(activity, mUseDefaultStunPreference.isChecked) false } } @@ -831,6 +864,29 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref } } + + private fun refreshIntegrationManagerSettings() { + val integrationAllowed = mSession.integrationManager.integrationAllowed + (findPreference(PreferencesManager.SETTINGS_INTEGRATION_ALLOW) as SwitchPreference).let { + val savedListener = it.onPreferenceChangeListener + it.onPreferenceChangeListener = null + it.isChecked = integrationAllowed + it.isEnabled = true + it.onPreferenceChangeListener = savedListener + } + + findPreference(PreferencesManager.SETTINGS_INTEGRATION_MANAGER_UI_URL).let { + if (integrationAllowed) { + it.summary = Matrix.getWidgetManager(context)?.uiUrl ?: getString(R.string.none) + it.isVisible = true + } else { + it.isVisible = false + } + } + + + } + private fun refreshBackgroundSyncSection(appContext: Context?) { // background sync tuning settings // these settings are useless and hidden if the app is registered to the FCM push service @@ -896,7 +952,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref // Even if using foreground service with foreground notif, it stops to work // in doze mode for certain devices :/ - if ( !isIgnoringBatteryOptimizations(requireContext())) { + if (!isIgnoringBatteryOptimizations(requireContext())) { requestDisablingBatteryOptimization(requireActivity(), this@VectorSettingsPreferencesFragment, REQUEST_BATTERY_OPTIMIZATION) @@ -905,11 +961,11 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref pushManager.setFdroidSyncModeOptimizedForRealTime(); } - PreferencesManager.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> { + PreferencesManager.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> { pushManager.setFdroidSyncModeOptimizedForBattery(); } - PreferencesManager.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { + PreferencesManager.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { pushManager.setFdroidSyncModeDisabled() } } @@ -1031,6 +1087,9 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this) + mSession.integrationManager.addListener(integrationManagerManagerListener) + refreshIntegrationManagerSettings() + // refresh anything else refreshPreferences() refreshNotificationPrivacy() @@ -1055,6 +1114,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref if (mSession.isAlive) { mSession.dataHandler.removeListener(mEventsListener) Matrix.getInstance(context)?.removeNetworkEventListener(mNetworkListener) + mSession.integrationManager.removeListener(integrationManagerManagerListener) } PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this) @@ -1973,6 +2033,31 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref addEmailBtn.order = order addEmailBtn.isVisible = true + addEmailBtn.isEnabled = false + //We need to check if the password flow is available + mSession.identityServerManager.checkAdd3pidInteractiveFlow(listOf(LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD), + object : ApiCallback { + override fun onSuccess(info: IdentityServerManager.SupportedFlowResult) { + addEmailBtn.isEnabled = info == IdentityServerManager.SupportedFlowResult.SUPPORTED + || info == IdentityServerManager.SupportedFlowResult.INTERACTIVE_AUTH_NOT_SUPPORTED + } + + override fun onUnexpectedError(e: java.lang.Exception?) { + //In doubt enable + addEmailBtn.isEnabled = true + } + + override fun onNetworkError(e: java.lang.Exception?) { + //In doubt enable + addEmailBtn.isEnabled = true + } + + override fun onMatrixError(e: MatrixError?) { + //In doubt enable + addEmailBtn.isEnabled = true + } + + }) } } @@ -1983,6 +2068,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref * @param errorMessage the error message */ private fun onCommonDone(errorMessage: String?) { + if (!isAdded) return activity?.runOnUiThread { if (!TextUtils.isEmpty(errorMessage) && errorMessage != null) { VectorApp.getInstance().toast(errorMessage) @@ -2017,7 +2103,7 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref mSession.identityServerManager.startAddSessionForEmail(pid, null, object : ApiCallback { override fun onSuccess(info: ThreePid) { - activity?.runOnUiThread { showEmailValidationDialog(pid) } + activity?.runOnUiThread { showEmailValidationDialog(pid, null) } } override fun onNetworkError(e: Exception) { @@ -2043,50 +2129,102 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref * * @param pid the used pid. */ - private fun showEmailValidationDialog(pid: ThreePid) { - activity?.let { - AlertDialog.Builder(it) - .setTitle(R.string.account_email_validation_title) - .setMessage(R.string.account_email_validation_message) - .setPositiveButton(R.string._continue) { _, _ -> - mSession.identityServerManager.finalizeAddSessionForEmail(pid, object : ApiCallback { - override fun onSuccess(info: Void?) { - it.runOnUiThread { - hideLoadingView() - mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback(){ - override fun onSuccess(info: Void?) { - refreshEmailsList() - } - }) - } - } + private fun showEmailValidationDialog(pid: ThreePid, password: String?) { + activity?.let { fragmentActivity -> + val auth = password?.let { + AuthParamsLoginPassword().apply { + this.user = mSession.myUserId + this.password = password + } + } + showEmailValidationDialog(fragmentActivity, pid, auth) + } + } - override fun onNetworkError(e: Exception) { - onCommonDone(e.localizedMessage) - } + private fun showEmailValidationDialog(fragmentActivity: FragmentActivity, pid: ThreePid, auth: AuthParamsLoginPassword?) { + AlertDialog.Builder(fragmentActivity) + .setTitle(R.string.account_email_validation_title) + .setMessage(R.string.account_email_validation_message) + .setPositiveButton(R.string._continue) { _, _ -> + finalizeAdd(pid, auth, fragmentActivity) + } + .setNegativeButton(R.string.cancel) { _, _ -> + hideLoadingView() + } + .show() + } - override fun onMatrixError(e: MatrixError) { - if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) { - it.runOnUiThread { - hideLoadingView() - it.toast(R.string.account_email_validation_error) + private fun finalizeAdd(pid: ThreePid, auth: AuthParamsLoginPassword?, fragmentActivity: FragmentActivity) { + mSession.identityServerManager.finalize3pidAddSession(pid, auth, object : ApiCallback { + override fun onSuccess(info: Void?) { + if (!isAdded) return + fragmentActivity.runOnUiThread { + hideLoadingView() + mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback() { + override fun onSuccess(info: Void?) { + refreshEmailsList() + } + }) + } + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (!isAdded) return + if (e.mStatus == 401 && e.mErrorBodyAsString.isNullOrBlank().not()) { + val flow = JsonUtils.toRegistrationFlowResponse(e.mErrorBodyAsString) + if (flow != null) { + val supportsLoginPassword = flow.flows.any { it.stages == listOf(LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD) } + if (supportsLoginPassword) { + //we prompt for it + val invalidPassError = requireContext().getString(R.string.login_error_forbidden) + .takeIf { e.errcode == MatrixError.FORBIDDEN } + DialogUtils.promptPassword(requireContext(), + invalidPassError, + auth?.let { auth.password }, + { password -> + val authParams = AuthParamsLoginPassword().apply { + this.user = mSession.myUserId + this.password = password + } + finalizeAdd(pid, authParams, fragmentActivity) + }, + { + onCommonDone(getString(R.string.settings_add_3pid_authentication_needed)) } - } else { - onCommonDone(e.localizedMessage) - } - } + ) + } else { + //you can only do that on mobile + AlertDialog.Builder(fragmentActivity) + .setMessage(R.string.settings_add_3pid_flow_not_supported) + .setPositiveButton(R.string._continue) { _, _ -> + onCommonDone(null) + } + .show() - override fun onUnexpectedError(e: Exception) { - onCommonDone(e.localizedMessage) - } - }) + } + } else { + onCommonDone(e.localizedMessage) } - .setNegativeButton(R.string.cancel) { _, _ -> - hideLoadingView() + } else if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) { + fragmentActivity.runOnUiThread { + fragmentActivity.toast(R.string.account_email_validation_error) + //Re-display the popup so that not everything is lost + showEmailValidationDialog(requireActivity(), pid, auth) } - .show() - } + } else { + onCommonDone(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) } //============================================================================================================== @@ -2219,6 +2357,31 @@ class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPref addPhoneBtn.order = order addPhoneBtn.isVisible = true + addPhoneBtn.isEnabled = false + //We need to check if the password flow is available + mSession.identityServerManager.checkAdd3pidInteractiveFlow(listOf(LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD), + object : ApiCallback { + override fun onSuccess(info: IdentityServerManager.SupportedFlowResult) { + addPhoneBtn.isEnabled = info == IdentityServerManager.SupportedFlowResult.SUPPORTED + || info == IdentityServerManager.SupportedFlowResult.INTERACTIVE_AUTH_NOT_SUPPORTED + } + + override fun onUnexpectedError(e: java.lang.Exception?) { + //In doubt enable + addPhoneBtn.isEnabled = true + } + + override fun onNetworkError(e: java.lang.Exception?) { + //In doubt enable + addPhoneBtn.isEnabled = true + } + + override fun onMatrixError(e: MatrixError?) { + //In doubt enable + addPhoneBtn.isEnabled = true + } + + }) } } diff --git a/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetFragment.kt b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetFragment.kt new file mode 100644 index 0000000000..f7d76e779c --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetFragment.kt @@ -0,0 +1,353 @@ +/* + * Copyright 2019 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.fragments.roomwidgets + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.webkit.* +import android.widget.ProgressBar +import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import butterknife.BindView +import com.airbnb.mvrx.* +import im.vector.R +import im.vector.activity.HandleBackParticipant +import im.vector.activity.ReviewTermsActivity +import im.vector.activity.util.TERMS_REQUEST_CODE +import im.vector.fragments.VectorBaseMvRxFragment +import im.vector.ui.themes.ThemeUtils +import im.vector.util.openUrlInExternalBrowser +import org.matrix.androidsdk.features.terms.TermsManager + +class RoomWidgetFragment : VectorBaseMvRxFragment(), HandleBackParticipant { + + var mWidgetWebView: WebView? = null + + @BindView(R.id.webview_error_layout) + lateinit var errorContainer: ViewGroup + + @BindView(R.id.webview_error_text) + lateinit var errorText: TextView + + @BindView(R.id.widget_progress_bar) + lateinit var webProgressBar: ProgressBar + + val viewModel: RoomWidgetViewModel by activityViewModel() + + override fun getLayoutResId(): Int = R.layout.fragment_room_widget + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + configureWebView() + setHasOptionsMenu(true) + viewModel.loadWebURLEvent.observe(this, Observer { + it?.getContentIfNotHandled()?.let { + mWidgetWebView?.clearHistory() + mWidgetWebView?.loadUrl(it) + mWidgetWebView?.isVisible = true + } + }) + + viewModel.termsNotSignedEvent.observe(this, Observer { termsEvent -> + termsEvent?.getContentIfNotHandled()?.let { + viewModel.widgetsManager?.uiUrl?.let { uiUrl -> + startActivityForResult(ReviewTermsActivity.intent(requireContext(), + TermsManager.ServiceType.IntegrationManager, uiUrl, it.token), + TERMS_REQUEST_CODE) + } + } + }) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == TERMS_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + viewModel.refreshAfterTermsAccepted() + } else { + viewModel.doFinish() + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mWidgetWebView = view.findViewById(R.id.widget_web_view) + } + + override fun onDestroy() { + //Looks like WebView creates memory leaks (?) according to legacy code + //We have to take additional steps to clear memory + destroyWebView() + super.onDestroy() + } + + private fun destroyWebView() { + + val webview = mWidgetWebView ?: return + // Make sure you remove the WebView from its parent view before doing anything. + (webview.parent as? ViewGroup)?.removeAllViews() + + webview.webViewClient = null + webview.clearHistory() + + // NOTE: clears RAM cache, if you pass true, it will also clear the disk cache. + webview.clearCache(true) + + // Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it. + webview.loadUrl("about:blank") + + webview.onPause() + webview.removeAllViews() + + // NOTE: This pauses JavaScript execution for ALL WebViews, + // do not use if you have other WebViews still alive. + // If you create another WebView after calling this, + // make sure to call mWebView.resumeTimers(). + webview.pauseTimers() + + // NOTE: This can occasionally cause a segfault below API 17 (4.2) + webview.destroy() + + // Null out the reference so that you don't end up re-using it. + mWidgetWebView = null + } + + private var webViewClient = object : WebViewClient() { + + var isInError = false + var currentPage: String? = null + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { +// Log.d("WEBVIEW", "onPageStarted $url") + isInError = false + currentPage = url + viewModel.webviewStartedToLoad(url) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) { +// Log.d("WEBVIEW", "onReceivedHttpError ${request?.url}") + if (request?.url.toString() != currentPage) { + // Ignore this error + return + } + isInError = true + viewModel.webviewLoadingError(request?.url?.toString(), + errorResponse?.reasonPhrase?.takeIf { it.isNotBlank() } + ?: errorResponse?.statusCode.toString()) + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + isInError = true + viewModel.webviewLoadingError(failingUrl, description) + } + + override fun onPageFinished(view: WebView?, url: String?) { +// Log.d("WEBVIEW", "onPageFinished ${url}") + if (!isInError) { + viewModel.webviewLoadSuccess(url) + } + } + } + + + override fun invalidate() = withState(viewModel) { state -> + // mWidgetTypeTextView.text = it.widgetName + when (state.status) { + WidgetState.UNKNOWN -> { + //Hide all? + mWidgetWebView?.isVisible = false + } + WidgetState.WIDGET_NOT_ALLOWED -> { + mWidgetWebView?.isVisible = false + } + WidgetState.WIDGET_ALLOWED -> { + mWidgetWebView?.isVisible = true + when (state.formattedURL) { + Uninitialized -> { + } + is Loading -> { + setError(null) + webProgressBar.isIndeterminate = true + webProgressBar.isVisible = true + } + is Success -> { + setError(null) + when (state.webviewLoadedUrl) { + Uninitialized -> { + mWidgetWebView?.isInvisible = true + } + is Loading -> { + setError(null) + mWidgetWebView?.isInvisible = false + webProgressBar.isIndeterminate = true + webProgressBar.isVisible = true + } + is Success -> { + mWidgetWebView?.isInvisible = false + webProgressBar.isVisible = false + setError(null) + } + is Fail -> { + webProgressBar.isInvisible = true + setError(state.webviewLoadedUrl.error.message) + } + } + } + is Fail -> { + //we need to show Error + mWidgetWebView?.isInvisible = true + webProgressBar.isVisible = false + setError(state.formattedURL.error.message) + } + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state -> + when (item.itemId) { + R.id.action_close -> { + viewModel.doCloseWidget(requireContext()) + return@withState true + } + R.id.action_refresh -> if (state.formattedURL.complete) { + mWidgetWebView?.reload() + return@withState true + } + R.id.action_widget_open_ext -> if (state.formattedURL.complete) { + openUrlInExternalBrowser(requireContext(), state.formattedURL.invoke()) + return@withState true + } + R.id.action_revoke -> if (state.status == WidgetState.WIDGET_ALLOWED) { + viewModel.revokeWidget() + viewModel.doFinish() + return@withState true + } + } + return@withState super.onOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu?) = withState(viewModel) { state -> + menu?.findItem(R.id.action_close)?.isVisible = state.canManageWidgets + menu?.findItem(R.id.action_revoke)?.isVisible = state.status == WidgetState.WIDGET_ALLOWED && !state.createdByMe + super.onPrepareOptionsMenu(menu) + } + + override fun onBackPressed(): Boolean = withState(viewModel) { state -> + if (state.formattedURL.complete) { + if (mWidgetWebView?.canGoBack() == true) { + mWidgetWebView?.goBack() + return@withState true + } + } + return@withState false + } + + private fun setError(message: String?) { + if (message == null) { + errorContainer.isVisible = false + errorText.text = null + } else { + webProgressBar.isVisible = false + errorContainer.isVisible = true + mWidgetWebView?.isInvisible = true + errorText.text = getString(R.string.room_widget_failed_to_load, message) + } + } + + /** + * Load the widget call + */ + @SuppressLint("NewApi") + private fun configureWebView() { + mWidgetWebView?.let { webview -> + // xml value seems ignored + webview.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_bottom_nav_background_color)) + + // clear caches + webview.clearHistory() + webview.clearFormData() + webview.clearCache(true) + + webview.settings.let { settings -> + // does not cache the data + settings.cacheMode = WebSettings.LOAD_NO_CACHE + + // Enable Javascript + settings.javaScriptEnabled = true + + // Use WideViewport and Zoom out if there is no viewport defined + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + + // Enable pinch to zoom without the zoom buttons + settings.builtInZoomControls = true + + // Allow use of Local Storage + settings.domStorageEnabled = true + + settings.allowFileAccessFromFileURLs = true + settings.allowUniversalAccessFromFileURLs = true + + settings.displayZoomControls = false + } + + // Permission requests + webview.webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + activity?.let { + WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, it) + } + } + } + + webview.webViewClient = webViewClient + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptThirdPartyCookies(mWidgetWebView, false) + } + } + } + + override fun onResume() { + super.onResume() + mWidgetWebView?.let { + it.resumeTimers() + it.onResume() + } + } + + override fun onPause() { + super.onPause() + mWidgetWebView?.let { + it.pauseTimers() + it.onPause() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionBottomSheet.kt b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionBottomSheet.kt new file mode 100644 index 0000000000..b08dc865bf --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionBottomSheet.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2019 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.fragments.roomwidgets + +import android.content.DialogInterface +import android.os.Build +import android.os.Parcelable +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.BulletSpan +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.R +import im.vector.extensions.withArgs +import im.vector.fragments.VectorBaseBottomSheetDialogFragment +import im.vector.util.VectorUtils +import im.vector.widgets.Widget +import kotlinx.android.parcel.Parcelize + +class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override fun getLayoutResId(): Int = R.layout.bottom_sheet_room_widget_permission + + private val viewModel: RoomWidgetPermissionViewModel by fragmentViewModel() + + @BindView(R.id.bottom_sheet_widget_permission_shared_info) + lateinit var sharedInfoTextView: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_id) + lateinit var authorIdText: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_display_name) + lateinit var authorNameText: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_avatar) + lateinit var authorAvatarView: ImageView + + var onFinish: ((Boolean) -> Unit)? = null + + override fun invalidate() = withState(viewModel) { state -> + + authorIdText.text = state.authorId + authorNameText.text = state.authorName ?: "" + VectorUtils.loadUserAvatar(requireContext(), viewModel.session, authorAvatarView, + state.authorAvatarUrl, state.authorId, state.authorName) + + val domain = state.widgetDomain ?: "" + val infoBuilder = SpannableStringBuilder() + .append(getString( + R.string.room_widget_permission_webview_shared_info_title + .takeIf { state.isWebviewWidget } + ?: R.string.room_widget_permission_shared_info_title, + "'$domain'")) + infoBuilder.append("\n") + + state.permissionsList?.forEach { + infoBuilder.append("\n") + val bulletPoint = getString(it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + infoBuilder.append(bulletPoint, BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + val start = infoBuilder.length + infoBuilder.append(bulletPoint) + infoBuilder.setSpan( + BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), + start, + bulletPoint.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + infoBuilder.append("\n") + + sharedInfoTextView.text = infoBuilder + } + + @OnClick(R.id.bottom_sheet_widget_permission_decline_button) + fun doDecline() { + viewModel.blockWidget() + //optimistic dismiss + dismiss() + onFinish?.invoke(false) + } + + @OnClick(R.id.bottom_sheet_widget_permission_continue_button) + fun doAccept() { + viewModel.allowWidget() + onFinish?.invoke(true) + //optimistic dismiss + dismiss() + + } + + override fun onCancel(dialog: DialogInterface?) { + super.onCancel(dialog) + onFinish?.invoke(false) + } + + @Parcelize + data class FragArgs( + val widget: Widget, + val mxId: String + ) : Parcelable + + + companion object { + + fun newInstance(matrixId: String, widget: Widget) = RoomWidgetPermissionBottomSheet().withArgs { + putParcelable(MvRx.KEY_ARG, FragArgs(widget, matrixId)) + } + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionViewModel.kt new file mode 100644 index 0000000000..8d5c1c35d5 --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetPermissionViewModel.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2019 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.fragments.roomwidgets + +import com.airbnb.mvrx.BaseMvRxViewModel +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.Matrix +import im.vector.R +import im.vector.activity.JitsiCallActivity +import im.vector.util.extractDomain +import im.vector.widgets.Widget +import im.vector.widgets.WidgetsManager +import org.matrix.androidsdk.MXSession +import org.matrix.androidsdk.core.callback.SimpleApiCallback +import java.net.URL + +data class RoomWidgetPermissionViewState( + val authorId: String = "", + val authorAvatarUrl: String? = null, + val authorName: String? = null, + val isWebviewWidget: Boolean = true, + val widgetDomain: String? = null, + //List of @StringRes + val permissionsList: List? = null +) : MvRxState + +class RoomWidgetPermissionViewModel(val session: MXSession, val widget: Widget, initialState: RoomWidgetPermissionViewState) + : BaseMvRxViewModel(initialState, false) { + + fun allowWidget(onFinished: (() -> Unit)? = null) = withState { state -> + + if (state.isWebviewWidget) { + session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId + ?: "", true, object : SimpleApiCallback() { + + override fun onSuccess(info: Void?) { + onFinished?.invoke() + } + + }) + } else { + session.integrationManager.setNativeWidgetDomainAllowed("jitsi", state.widgetDomain + ?: "", true, object : SimpleApiCallback() { + + override fun onSuccess(info: Void?) { + onFinished?.invoke() + } + + }) + } + } + + fun blockWidget(onFinished: (() -> Unit)? = null) = withState { state -> + if (state.isWebviewWidget) { + session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId + ?: "", false, object : SimpleApiCallback() { + override fun onSuccess(info: Void?) { + onFinished?.invoke() + } + + }) + } else { + session.integrationManager.setNativeWidgetDomainAllowed("jitsi", state.widgetDomain + ?: "", false, object : SimpleApiCallback() { + + override fun onSuccess(info: Void?) { + onFinished?.invoke() + } + + }) + } + } + + companion object : MvRxViewModelFactory { + + val LOG_TAG = RoomWidgetPermissionViewModel::class.simpleName + + override fun create(viewModelContext: ViewModelContext, state: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel? { + val args = viewModelContext.args() + val session = Matrix.getMXSession(viewModelContext.activity, args.mxId) + return RoomWidgetPermissionViewModel(session, args.widget, state) + } + + override fun initialState(viewModelContext: ViewModelContext): RoomWidgetPermissionViewState? { + val args = viewModelContext.args() + val session = Matrix.getMXSession(viewModelContext.activity, args.mxId) + val widget = args.widget + + val room = session.dataHandler.getRoom(widget.roomId) + val creator = room.getMember(widget.widgetEvent.sender) + + var domain: String? + try { + domain = URL(widget.url).host + } catch (e: Throwable) { + domain = null + } + + if (WidgetsManager.isJitsiWidget(widget)) { + val infoShared = listOf( + R.string.room_widget_permission_display_name, + R.string.room_widget_permission_avatar_url + ) + return RoomWidgetPermissionViewState( + isWebviewWidget = false, + authorName = creator?.displayname, + authorId = widget.widgetEvent.sender, + authorAvatarUrl = creator?.getAvatarUrl(), + widgetDomain = extractDomain(JitsiCallActivity.JITSI_SERVER_URL), + permissionsList = infoShared + ) + + } else { + + //TODO check from widget urls the perms that should be shown? + //For now put all + val infoShared = listOf( + R.string.room_widget_permission_display_name, + R.string.room_widget_permission_avatar_url, + R.string.room_widget_permission_user_id, + R.string.room_widget_permission_theme, + R.string.room_widget_permission_widget_id, + R.string.room_widget_permission_room_id + ) + return RoomWidgetPermissionViewState( + authorName = creator?.displayname, + authorId = widget.widgetEvent.sender, + authorAvatarUrl = creator?.getAvatarUrl(), + widgetDomain = domain, + permissionsList = infoShared + ) + } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetViewModel.kt b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetViewModel.kt new file mode 100644 index 0000000000..8e0f47b265 --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/roomwidgets/RoomWidgetViewModel.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2019 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.fragments.roomwidgets + +import android.content.Context +import android.text.TextUtils +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.* +import im.vector.Matrix +import im.vector.R +import im.vector.VectorApp +import im.vector.activity.WidgetActivity +import im.vector.ui.arch.LiveEvent +import im.vector.widgets.Widget +import im.vector.widgets.WidgetsManager +import org.matrix.androidsdk.MXSession +import org.matrix.androidsdk.core.Log +import org.matrix.androidsdk.core.callback.ApiCallback +import org.matrix.androidsdk.core.model.MatrixError +import org.matrix.androidsdk.data.Room +import org.matrix.androidsdk.features.integrationmanager.IntegrationManager +import org.matrix.androidsdk.features.terms.TermsNotSignedException + +enum class WidgetState { + UNKNOWN, + WIDGET_NOT_ALLOWED, + WIDGET_ALLOWED +} + +data class RoomWidgetViewModelState( + val status: WidgetState = WidgetState.UNKNOWN, + val formattedURL: Async = Uninitialized, + val webviewLoadedUrl: Async = Uninitialized, + val widgetName: String = "", + val canManageWidgets: Boolean = false, + val createdByMe: Boolean = false +) : MvRxState + +class RoomWidgetViewModel(initialState: RoomWidgetViewModelState, val widget: Widget) + : BaseMvRxViewModel(initialState, false) { + + companion object : MvRxViewModelFactory { + const val NAVIGATE_FINISH = "NAVIGATE_FINISH" + + override fun create(viewModelContext: ViewModelContext, state: RoomWidgetViewModelState): RoomWidgetViewModel? { + return (viewModelContext.activity.intent?.extras?.getSerializable(WidgetActivity.EXTRA_WIDGET_ID) as? Widget)?.let { + RoomWidgetViewModel(state, it) + } ?: super.create(viewModelContext, state) + } + + override fun initialState(viewModelContext: ViewModelContext): RoomWidgetViewModelState? { + val widget = viewModelContext.activity.intent?.extras?.getSerializable(WidgetActivity.EXTRA_WIDGET_ID) as? Widget + ?: return null + val session = Matrix.getInstance(viewModelContext.activity).getSession(widget.sessionId) + return RoomWidgetViewModelState( + widgetName = widget.humanName, + createdByMe = widget.widgetEvent.getSender() == session?.myUserId + ) + } + + } + + var navigateEvent: MutableLiveData> = MutableLiveData() + var termsNotSignedEvent: MutableLiveData> = MutableLiveData() + var loadWebURLEvent: MutableLiveData> = MutableLiveData() + var toastMessageEvent: MutableLiveData> = MutableLiveData() + + private var room: Room? = null + + var session: MXSession? = null + + var widgetsManager: WidgetsManager? = null + + /** + * Widget events listener + */ + private val mWidgetListener = WidgetsManager.onWidgetUpdateListener { widget -> + if (TextUtils.equals(widget.widgetId, widget.widgetId)) { + if (!widget.isActive) { + doFinish() + } + } + } + + init { + configure() + + session?.integrationManager?.addListener(object : IntegrationManager.IntegrationManagerManagerListener { + override fun onIntegrationManagerChange(managerConfig: IntegrationManager) { + refreshPermissionStatus() + } + + }) + } + + fun webviewStartedToLoad(url: String?) = withState { + //Only do it for first load + setState { + copy(webviewLoadedUrl = Loading()) + } + } + + fun webviewLoadingError(url: String?, reason: String?) = withState { + setState { + copy(webviewLoadedUrl = Fail(Throwable(reason))) + } + } + + fun webviewLoadSuccess(url: String?) = withState { + setState { + copy(webviewLoadedUrl = Success(url ?: "")) + } + } + + private fun configure() { + + val applicationContext = VectorApp.getInstance().applicationContext + val matrix = Matrix.getInstance(applicationContext) + + session = matrix.getSession(widget.sessionId) + + if (session == null) { + //defensive code + doFinish() + return + } + + room = session?.dataHandler?.getRoom(widget.roomId) + + if (room == null) { + //defensive code + doFinish() + return + } + + widgetsManager = matrix + .getWidgetManagerProvider(session)?.getWidgetManager(applicationContext) + + setState { + copy(canManageWidgets = WidgetsManager.checkWidgetPermission(session, room) == null) + } + + + widgetsManager?.addListener(mWidgetListener) + + refreshPermissionStatus(applicationContext) + } + + private fun refreshPermissionStatus(applicationContext: Context = VectorApp.getInstance().applicationContext) { + + //If it was added by me, consider it as allowed + if (widget.widgetEvent.getSender() == session?.myUserId) { + onWidgetAllowed(applicationContext) + return + } + + val isAllowed = session + ?.integrationManager + ?.isWidgetAllowed(widget.widgetEvent.eventId) + ?: false + + if (!isAllowed) { + setState { + copy(status = WidgetState.WIDGET_NOT_ALLOWED) + } + } else { + //we can start loading the widget then + onWidgetAllowed(applicationContext) + } + } + + + fun doCloseWidget(context: Context) { + AlertDialog.Builder(context) + .setMessage(R.string.widget_delete_message_confirmation) + .setPositiveButton(R.string.remove) { _, _ -> + + widgetsManager?.closeWidget(session, room, widget.widgetId, object : ApiCallback { + override fun onSuccess(info: Void?) { + doFinish() + } + + private fun onError(errorMessage: String) { + toastMessageEvent.postValue(LiveEvent(errorMessage)) + } + + override fun onNetworkError(e: Exception) { + onError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onError(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onError(e.localizedMessage) + } + }) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + fun doFinish() { + navigateEvent.postValue(LiveEvent(NAVIGATE_FINISH)) + } + + fun refreshAfterTermsAccepted() { + onWidgetAllowed() + } + + private fun onWidgetAllowed(applicationContext: Context = VectorApp.getInstance().applicationContext) { + + setState { + copy( + status = WidgetState.WIDGET_ALLOWED, + formattedURL = Loading() + ) + } + if (widgetsManager != null) { + widgetsManager!!.getFormattedWidgetUrl(applicationContext, widget, object : ApiCallback { + override fun onSuccess(url: String) { + loadWebURLEvent.postValue(LiveEvent(url)) + setState { + //We use a live event to trigger the webview load + copy( + status = WidgetState.WIDGET_ALLOWED, + formattedURL = Success(url) + ) + } + } + + private fun onError(errorMessage: String) { + setState { + copy( + status = WidgetState.WIDGET_ALLOWED, + formattedURL = Fail(Throwable(errorMessage)) + ) + } + } + + override fun onNetworkError(e: Exception) { + onError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onError(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + if (e is TermsNotSignedException) { + termsNotSignedEvent.postValue(LiveEvent(e)) + } else { + onError(e.localizedMessage) + } + } + }) + } else { + setState { + copy( + status = WidgetState.WIDGET_ALLOWED, + formattedURL = Success(widget.url) + ) + } + loadWebURLEvent.postValue(LiveEvent(widget.url)) + } + } + + fun revokeWidget(onFinished: (() -> Unit)? = null) { + setState { + copy( + status = WidgetState.UNKNOWN + ) + } + session?.integrationManager?.setWidgetAllowed(widget.widgetEvent?.eventId + ?: "", false, object : ApiCallback { + override fun onSuccess(info: Void?) { + onFinished?.invoke() + } + + override fun onUnexpectedError(e: Exception) { + Log.e(this::class.java.name, e.message) + } + + override fun onNetworkError(e: Exception) { + Log.e(this::class.java.name, e.message) + } + + override fun onMatrixError(e: MatrixError) { + Log.e(this::class.java.name, e.message) + } + + }) + } + + override fun onCleared() { + super.onCleared() + widgetsManager?.removeListener(mWidgetListener) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/fragments/roomwidgets/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/fragments/roomwidgets/WebviewPermissionUtils.kt new file mode 100644 index 0000000000..74d79f775f --- /dev/null +++ b/vector/src/main/java/im/vector/fragments/roomwidgets/WebviewPermissionUtils.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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.fragments.roomwidgets + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.webkit.PermissionRequest +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import im.vector.R + +object WebviewPermissionUtils { + + @SuppressLint("NewApi") + fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, activity: Activity) { + if (activity.isFinishing || activity.isDestroyed) return + val allowedPermissions = request.resources.map { + it to false + }.toMutableList() + AlertDialog.Builder(activity) + .setTitle(title) + .setMultiChoiceItems( + request.resources.map { webPermissionToHumanReadable(it, activity) }.toTypedArray() + , null + ) { dialog, which, isChecked -> + allowedPermissions[which] = allowedPermissions[which].first to isChecked + } + .setPositiveButton(R.string.room_widget_resource_grant_permission) { dialog, wich -> + request.grant(allowedPermissions.mapNotNull { perm -> + perm.first.takeIf { perm.second } + }.toTypedArray()) + } + .setNegativeButton(R.string.room_widget_resource_decline_permission) { dialog, wich -> + request.deny() + } + .show() + } + + private fun webPermissionToHumanReadable(permission: String, context: Context): String { + return when (permission) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone) + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> context.getString(R.string.room_widget_webview_access_camera) + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> context.getString(R.string.room_widget_webview_read_protected_media) + else -> permission + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/fragments/terms/AcceptTermsFragment.kt b/vector/src/main/java/im/vector/fragments/terms/AcceptTermsFragment.kt index 4b244feef5..719d9abe23 100644 --- a/vector/src/main/java/im/vector/fragments/terms/AcceptTermsFragment.kt +++ b/vector/src/main/java/im/vector/fragments/terms/AcceptTermsFragment.kt @@ -19,7 +19,6 @@ import android.app.Activity import android.os.Bundle import android.view.ViewGroup import android.widget.Button -import android.widget.ProgressBar import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.lifecycle.Observer @@ -48,8 +47,8 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener { @BindView(R.id.terms_bottom_accept) lateinit var acceptButton: Button - @BindView(R.id.termsLoadingIndicator) - lateinit var progressBar: ProgressBar + @BindView(R.id.waitOverlay) + lateinit var waitingModalOverlay: ViewGroup @BindView(R.id.termsBottomBar) lateinit var bottomBar: ViewGroup @@ -73,10 +72,10 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener { when (terms) { is MxAsync.Loading -> { bottomBar.isVisible = false - progressBar.isVisible = true + waitingModalOverlay.isVisible = true } is MxAsync.Error -> { - progressBar.isVisible = false + waitingModalOverlay.isVisible = false terms.stringResId.let { stringRes -> AlertDialog.Builder(requireActivity()) .setMessage(stringRes) @@ -88,20 +87,23 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener { } is MxAsync.Success -> { updateState(terms.value) - progressBar.isVisible = false + waitingModalOverlay.isVisible = false bottomBar.isVisible = true acceptButton.isEnabled = terms.value.all { it.accepted } } + else -> { + waitingModalOverlay.isVisible = false + } } }) viewModel.acceptTerms.observe(this, Observer { request -> when (request) { is MxAsync.Loading -> { - progressBar.isVisible = true + waitingModalOverlay.isVisible = true } is MxAsync.Error -> { - progressBar.isVisible = false + waitingModalOverlay.isVisible = false request.stringResId.let { stringRes -> AlertDialog.Builder(requireActivity()) .setMessage(stringRes) @@ -110,9 +112,14 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener { } } is MxAsync.Success -> { + waitingModalOverlay.isVisible = false activity?.setResult(Activity.RESULT_OK) activity?.finish() } + else -> { + waitingModalOverlay.isVisible = false + } + } }) } diff --git a/vector/src/main/java/im/vector/settings/VectorLocale.kt b/vector/src/main/java/im/vector/settings/VectorLocale.kt index 139f6246eb..3ba74c1945 100644 --- a/vector/src/main/java/im/vector/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/settings/VectorLocale.kt @@ -20,9 +20,8 @@ import android.content.Context import android.content.res.Configuration import android.os.Build import android.preference.PreferenceManager -import android.text.TextUtils -import android.util.Pair import androidx.core.content.edit +import im.vector.BuildConfig import im.vector.R import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -38,6 +37,7 @@ object VectorLocale { private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" + private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" private val defaultLocale = Locale("en", "US") @@ -69,7 +69,7 @@ object VectorLocale { // detect if the default language is used val defaultStringValue = getString(context, defaultLocale, R.string.resources_country_code) - if (TextUtils.equals(defaultStringValue, getString(context, applicationLocale, R.string.resources_country_code))) { + if (defaultStringValue == getString(context, applicationLocale, R.string.resources_country_code)) { applicationLocale = defaultLocale } @@ -90,25 +90,34 @@ object VectorLocale { PreferenceManager.getDefaultSharedPreferences(context).edit { val language = locale.language - if (TextUtils.isEmpty(language)) { + if (language.isEmpty()) { remove(APPLICATION_LOCALE_LANGUAGE_KEY) } else { putString(APPLICATION_LOCALE_LANGUAGE_KEY, language) } val country = locale.country - if (TextUtils.isEmpty(country)) { + if (country.isEmpty()) { remove(APPLICATION_LOCALE_COUNTRY_KEY) } else { putString(APPLICATION_LOCALE_COUNTRY_KEY, country) } val variant = locale.variant - if (TextUtils.isEmpty(variant)) { + if (variant.isEmpty()) { remove(APPLICATION_LOCALE_VARIANT_KEY) } else { putString(APPLICATION_LOCALE_VARIANT_KEY, variant) } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val script = locale.script + if (script.isEmpty()) { + remove(APPLICATION_LOCALE_SCRIPT_KEY) + } else { + putString(APPLICATION_LOCALE_SCRIPT_KEY, script) + } + } } } @@ -157,24 +166,43 @@ object VectorLocale { * @param context the context */ private fun initApplicationLocales(context: Context) { - val knownLocalesSet = HashSet>() + val knownLocalesSet = HashSet>() try { val availableLocales = Locale.getAvailableLocales() for (locale in availableLocales) { - knownLocalesSet.add(Pair(getString(context, locale, R.string.resources_language), - getString(context, locale, R.string.resources_country_code))) + knownLocalesSet.add( + Triple( + getString(context, locale, R.string.resources_language), + getString(context, locale, R.string.resources_country_code), + getString(context, locale, R.string.resources_script) + )) } } catch (e: Exception) { Log.e(LOG_TAG, "## getApplicationLocales() : failed " + e.message, e) - knownLocalesSet.add(Pair(context.getString(R.string.resources_language), context.getString(R.string.resources_country_code))) + knownLocalesSet.add( + Triple( + context.getString(R.string.resources_language), + context.getString(R.string.resources_country_code), + context.getString(R.string.resources_script) + )) } supportedLocales.clear() for (knownLocale in knownLocalesSet) { - supportedLocales.add(Locale(knownLocale.first, knownLocale.second)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + supportedLocales.add( + Locale.Builder() + .setLanguage(knownLocale.first) + .setRegion(knownLocale.second) + .setScript(knownLocale.third) + .build() + ) + } else { + supportedLocales.add(Locale(knownLocale.first, knownLocale.second)) + } } // sort by human display names @@ -188,13 +216,38 @@ object VectorLocale { * @return the string */ fun localeToLocalisedString(locale: Locale): String { - var res = locale.getDisplayLanguage(locale) + return buildString { + append(locale.getDisplayLanguage(locale)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && locale.script != "Latn" + && locale.getDisplayScript(locale).isNotEmpty()) { + append(" - ") + append(locale.getDisplayScript(locale)) + } - if (!TextUtils.isEmpty(locale.getDisplayCountry(locale))) { - res += " (" + locale.getDisplayCountry(locale) + ")" - } + if (locale.getDisplayCountry(locale).isNotEmpty()) { + append(" (") + append(locale.getDisplayCountry(locale)) + append(")") + } - return res + // In debug mode, also display information about the locale in the current locale. + if (BuildConfig.DEBUG) { + append("\n[") + append(locale.displayLanguage) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { + append(" - ") + append(locale.displayScript) + } + if (locale.displayCountry.isNotEmpty()) { + append(" (") + append(locale.displayCountry) + append(")") + } + append("]") + } + } } } diff --git a/vector/src/main/java/im/vector/store/LoginStorage.java b/vector/src/main/java/im/vector/store/LoginStorage.java index 28f7daaa28..bfb7c04264 100755 --- a/vector/src/main/java/im/vector/store/LoginStorage.java +++ b/vector/src/main/java/im/vector/store/LoginStorage.java @@ -199,6 +199,7 @@ public void clear() { SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.remove(PREFS_KEY_CONNECTION_CONFIGS); - editor.apply(); + //Need to commit now because called before forcing an app restart + editor.commit(); } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/util/PreferencesManager.java b/vector/src/main/java/im/vector/util/PreferencesManager.java index e6ee643bec..fb4c7aea07 100755 --- a/vector/src/main/java/im/vector/util/PreferencesManager.java +++ b/vector/src/main/java/im/vector/util/PreferencesManager.java @@ -170,10 +170,8 @@ public class PreferencesManager { public static final String SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"; //Integrations + public static final String SETTINGS_INTEGRATION_ALLOW = "SETTINGS_INTEGRATION_ALLOW"; public static final String SETTINGS_INTEGRATION_MANAGER_UI_URL = "SETTINGS_INTEGRATION_MANAGER_UI_URL"; - public static final String SETTINGS_INTEGRATION_MANAGER_API_URL = "SETTINGS_INTEGRATION_MANAGER_API_URL"; - public static final String SETTINGS_INTEGRATION_MANAGER_JITSI_URL = "SETTINGS_INTEGRATION_MANAGER_JITSI_URL"; - public static final String SETTINGS_INTEGRATION_WHITELIST_URL = "SETTINGS_INTEGRATION_WHITELIST_URL"; // other public static final String SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"; @@ -289,51 +287,6 @@ public static void setDidAskUserToIgnoreBatteryOptimizations(Context context) { .apply(); } - public static String getIntegrationManagerUiUrl(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_INTEGRATION_MANAGER_UI_URL, - context.getString(R.string.integrations_ui_url)); - } - - public static String getIntegrationManagerApiUrl(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_INTEGRATION_MANAGER_API_URL, - context.getString(R.string.integrations_rest_url)); - } - - public static String getIntegrationManagerJitsiUrl(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_INTEGRATION_MANAGER_JITSI_URL, - context.getString(R.string.integrations_jitsi_widget_url)); - } - - - public static void setIntegrationManagerUrls(Context context, String uiURl, String apiURl, String jitsiUrl) { - if (uiURl != null) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putString(SETTINGS_INTEGRATION_MANAGER_UI_URL, uiURl) - .apply(); - } - if (apiURl != null) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putString(SETTINGS_INTEGRATION_MANAGER_API_URL, apiURl) - .apply(); - } - - if (jitsiUrl != null) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putString(SETTINGS_INTEGRATION_MANAGER_JITSI_URL, jitsiUrl) - .apply(); - } - } - - public static List getIntegrationWhiteListedUrl(Context context) { - Set defaultSet = new HashSet<>(Arrays.asList(context.getResources().getStringArray(R.array.integrations_widgets_urls))); - Set set = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(SETTINGS_INTEGRATION_WHITELIST_URL, defaultSet); - return new ArrayList<>(set); - } - - public static boolean didMigrateToNotificationRework(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false); } diff --git a/vector/src/main/java/im/vector/util/SlashCommandsParser.java b/vector/src/main/java/im/vector/util/SlashCommandsParser.java index c16ee5d827..710ca5342e 100755 --- a/vector/src/main/java/im/vector/util/SlashCommandsParser.java +++ b/vector/src/main/java/im/vector/util/SlashCommandsParser.java @@ -18,12 +18,14 @@ package im.vector.util; +import android.app.Activity; import android.text.TextUtils; import android.widget.Toast; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import org.jetbrains.annotations.Nullable; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.core.Log; import org.matrix.androidsdk.core.callback.ApiCallback; @@ -38,6 +40,7 @@ import java.util.HashMap; import java.util.Map; +import im.vector.Matrix; import im.vector.R; import im.vector.VectorApp; import im.vector.activity.CommonActivityUtils; @@ -353,7 +356,7 @@ public void onMatrixError(final MatrixError e) { isIRCCmd = true; isIRCCmdValid = true; - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(activity); + WidgetsManager wm = getWidgetManager(activity); if (wm != null) { wm.clearScalarToken(activity, session); Toast.makeText(activity, "Scalar token cleared", Toast.LENGTH_SHORT).show(); @@ -384,4 +387,14 @@ public void onMatrixError(final MatrixError e) { return isIRCCmd; } + + @Nullable + private static WidgetsManager getWidgetManager(Activity activity) { + if (Matrix.getInstance(activity) == null) return null; + MXSession session = Matrix.getInstance(activity).getDefaultSession(); + if (session == null) return null; + WidgetManagerProvider widgetManagerProvider = Matrix.getInstance(activity).getWidgetManagerProvider(session); + if (widgetManagerProvider == null) return null; + return widgetManagerProvider.getWidgetManager(activity); + } } diff --git a/vector/src/main/java/im/vector/util/UrlUtil.kt b/vector/src/main/java/im/vector/util/UrlUtil.kt index d40f27e5e8..7eb095cf27 100644 --- a/vector/src/main/java/im/vector/util/UrlUtil.kt +++ b/vector/src/main/java/im/vector/util/UrlUtil.kt @@ -16,6 +16,9 @@ package im.vector.util +import java.net.MalformedURLException +import java.net.URL + /** * Schemes */ @@ -41,4 +44,12 @@ fun removeUrlScheme(aUrl: String?): String? { } return urlRetValue +} + +fun extractDomain(aUrl: String?): String? { + try { + return aUrl?.let { URL(it).host } + } catch (e : MalformedURLException) { + return null + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/view/ActiveWidgetsBanner.java b/vector/src/main/java/im/vector/view/ActiveWidgetsBanner.java index f833a2d3df..22d37f91cf 100755 --- a/vector/src/main/java/im/vector/view/ActiveWidgetsBanner.java +++ b/vector/src/main/java/im/vector/view/ActiveWidgetsBanner.java @@ -18,12 +18,14 @@ package im.vector.view; +import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; +import org.jetbrains.annotations.Nullable; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.core.Log; import org.matrix.androidsdk.data.Room; @@ -31,6 +33,7 @@ import java.util.ArrayList; import java.util.List; +import im.vector.Matrix; import im.vector.R; import im.vector.widgets.Widget; import im.vector.widgets.WidgetManagerProvider; @@ -168,12 +171,8 @@ public void setOnUpdateListener(onUpdateListener listener) { * Refresh the view visibility */ private void refresh() { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); - if (wm == null) { - return; - } if ((null != mRoom) && (null != mSession)) { - List activeWidgets = wm.getActiveWebviewWidgets(mSession, mRoom); + List activeWidgets = WidgetsManager.getActiveWebviewWidgets(mSession, mRoom); Widget firstWidget = null; if ((activeWidgets.size() != mActiveWidgets.size()) || !mActiveWidgets.containsAll(activeWidgets)) { @@ -199,7 +198,7 @@ private void refresh() { setVisibility((mActiveWidgets.size() > 0) ? View.VISIBLE : View.GONE); // show the close widget button if the user is allowed to do it - mCloseWidgetIcon.setVisibility(((null != firstWidget) && (null == wm.checkWidgetPermission(mSession, mRoom))) ? + mCloseWidgetIcon.setVisibility(((null != firstWidget) && (null == WidgetsManager.checkWidgetPermission(mSession, mRoom))) ? View.VISIBLE : View.GONE); } } @@ -209,7 +208,7 @@ private void refresh() { */ public void onActivityResume() { refresh(); - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); + WidgetsManager wm = getWidgetManager(getContext()); if (wm != null) { wm.addListener(mWidgetListener); } @@ -219,9 +218,19 @@ public void onActivityResume() { * The parent activity is suspended */ public void onActivityPause() { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); + WidgetsManager wm = getWidgetManager(getContext()); if (wm != null) { wm.removeListener(mWidgetListener); } } + + @Nullable + private WidgetsManager getWidgetManager(Context activity) { + if (Matrix.getInstance(activity) == null) return null; + MXSession session = Matrix.getInstance(activity).getDefaultSession(); + if (session == null) return null; + WidgetManagerProvider widgetManagerProvider = Matrix.getInstance(activity).getWidgetManagerProvider(session); + if (widgetManagerProvider == null) return null; + return widgetManagerProvider.getWidgetManager(activity); + } } diff --git a/vector/src/main/java/im/vector/view/VectorOngoingConferenceCallView.java b/vector/src/main/java/im/vector/view/VectorOngoingConferenceCallView.java index 517b52e3c0..5c6852c014 100755 --- a/vector/src/main/java/im/vector/view/VectorOngoingConferenceCallView.java +++ b/vector/src/main/java/im/vector/view/VectorOngoingConferenceCallView.java @@ -31,6 +31,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import org.jetbrains.annotations.Nullable; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.call.IMXCall; import org.matrix.androidsdk.call.IMXCallsManagerListener; @@ -44,6 +45,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import im.vector.Matrix; import im.vector.R; import im.vector.widgets.Widget; import im.vector.widgets.WidgetManagerProvider; @@ -241,12 +243,8 @@ public void setCallClickListener(ICallClickListener callClickListener) { * Refresh the view visibility */ public void refresh() { - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); - if (wm == null) { - return; - } if ((null != mRoom) && (null != mSession)) { - List mActiveWidgets = wm.getActiveJitsiWidgets(mSession, mRoom); + List mActiveWidgets = WidgetsManager.getActiveJitsiWidgets(mSession, mRoom); Widget widget = mActiveWidgets.isEmpty() ? null : mActiveWidgets.get(0); if (mActiveWidget != widget) { @@ -264,7 +262,7 @@ public void refresh() { setVisibility(((!MXCallsManager.isCallInProgress(call) && mRoom.isOngoingConferenceCall()) || (null != mActiveWidget)) ? View.VISIBLE : View.GONE); // show the close widget button if the user is allowed to do it - mCloseWidgetIcon.setVisibility(((null != mActiveWidget) && (null == wm.checkWidgetPermission(mSession, mRoom))) ? + mCloseWidgetIcon.setVisibility(((null != mActiveWidget) && (null == WidgetsManager.checkWidgetPermission(mSession, mRoom))) ? View.VISIBLE : View.GONE); } } @@ -278,7 +276,7 @@ public void onActivityResume() { if (null != mSession) { mSession.mCallsManager.addListener(mCallsListener); } - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); + WidgetsManager wm = Matrix.getWidgetManager(getContext()); if (wm != null) { wm.addListener(mWidgetListener); } @@ -291,7 +289,7 @@ public void onActivityPause() { if (null != mSession) { mSession.mCallsManager.removeListener(mCallsListener); } - WidgetsManager wm = WidgetManagerProvider.INSTANCE.getWidgetManager(getContext()); + WidgetsManager wm = Matrix.getWidgetManager(getContext()); if (wm != null) { wm.removeListener(mWidgetListener); } @@ -303,4 +301,5 @@ public void onActivityPause() { public Widget getActiveWidget() { return mActiveWidget; } + } diff --git a/vector/src/main/java/im/vector/widgets/IntegrationManagerConfig.kt b/vector/src/main/java/im/vector/widgets/IntegrationManagerConfig.kt index d38aeba552..0eda53b71b 100644 --- a/vector/src/main/java/im/vector/widgets/IntegrationManagerConfig.kt +++ b/vector/src/main/java/im/vector/widgets/IntegrationManagerConfig.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package im.vector.widgets /** diff --git a/vector/src/main/java/im/vector/widgets/WidgetError.java b/vector/src/main/java/im/vector/widgets/WidgetError.java new file mode 100644 index 0000000000..cb97410cc2 --- /dev/null +++ b/vector/src/main/java/im/vector/widgets/WidgetError.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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.widgets; + +import org.matrix.androidsdk.core.model.MatrixError; + +/** + * Widget error code + */ +public class WidgetError extends MatrixError { + public static final String WIDGET_NOT_ENOUGH_POWER_ERROR_CODE = "WIDGET_NOT_ENOUGH_POWER_ERROR_CODE"; + public static final String WIDGET_CREATION_FAILED_ERROR_CODE = "WIDGET_CREATION_FAILED_ERROR_CODE"; + + /** + * Create a widget error + * + * @param code the error code (see XX_ERROR_CODE) + * @param detailedErrorDescription the detailed error description + */ + public WidgetError(String code, String detailedErrorDescription) { + errcode = code; + error = detailedErrorDescription; + } +} diff --git a/vector/src/main/java/im/vector/widgets/WidgetManagerProvider.kt b/vector/src/main/java/im/vector/widgets/WidgetManagerProvider.kt index 8839b1af4b..ea24f173b8 100644 --- a/vector/src/main/java/im/vector/widgets/WidgetManagerProvider.kt +++ b/vector/src/main/java/im/vector/widgets/WidgetManagerProvider.kt @@ -16,36 +16,84 @@ package im.vector.widgets import android.content.Context -import im.vector.util.PreferencesManager +import im.vector.R +import org.matrix.androidsdk.MXSession +import org.matrix.androidsdk.features.integrationmanager.IntegrationManager import java.net.MalformedURLException import java.net.URL -object WidgetManagerProvider { +class WidgetManagerProvider(private val session: MXSession) : IntegrationManager.IntegrationManagerManagerListener { + + + override fun onIntegrationManagerChange(managerConfig: IntegrationManager) { + //Clear caches + currentConfig = null + widgetsManager = null + } + + init { + session.integrationManager.addListener(this) + } private var widgetsManager: WidgetsManager? = null + private var currentConfig: IntegrationManagerConfig? = null fun getWidgetManager(context: Context): WidgetsManager? { - if (widgetsManager != null) { + if (!session.integrationManager.integrationAllowed) return null + + val userDefinedConfig = session.integrationManager.integrationServerConfig + val sdkConfig: IntegrationManagerConfig? + val defaultWhitelist = context.resources.getStringArray(R.array.integrations_widgets_urls).asList() + + if (userDefinedConfig != null) { + + val whiteList = defaultWhitelist + userDefinedConfig.apiUrl + sdkConfig = IntegrationManagerConfig( + uiUrl = userDefinedConfig.uiUrl, + apiUrl = userDefinedConfig.apiUrl, + jitsiUrl = "${userDefinedConfig.apiUrl}/widgets/jitsi.html", + whiteListedUrls = whiteList) + } else { + //Use the default IM + // Check if a config is defined in wellknown + val wellKnownIM = session.integrationManager.getWellKnownIntegrationManagerConfigs().firstOrNull() + sdkConfig = if (wellKnownIM != null) { + IntegrationManagerConfig( + uiUrl = wellKnownIM.uiUrl, + apiUrl = wellKnownIM.apiUrl, + jitsiUrl = "${wellKnownIM.apiUrl}/widgets/jitsi.html", + whiteListedUrls = listOf(wellKnownIM.apiUrl) + ) + } else { + IntegrationManagerConfig( + uiUrl = context.getString(R.string.integrations_ui_url), + apiUrl = context.getString(R.string.integrations_rest_url), + jitsiUrl = context.getString(R.string.integrations_jitsi_widget_url), + whiteListedUrls = defaultWhitelist) + } + } + + + + if (currentConfig == sdkConfig && widgetsManager != null) { return widgetsManager } - val uiURl = PreferencesManager.getIntegrationManagerUiUrl(context) - val apiURL = PreferencesManager.getIntegrationManagerApiUrl(context) - val jitsiUrl = PreferencesManager.getIntegrationManagerJitsiUrl(context) - if (uiURl.isNullOrBlank() || apiURL.isNullOrBlank() || jitsiUrl.isNullOrBlank()) return null + + if (sdkConfig.uiUrl.isBlank() || sdkConfig.apiUrl.isBlank() || sdkConfig.jitsiUrl.isBlank()) { + currentConfig = null + widgetsManager = null + return null + } + return try { //Very basic validity check (well formed url) - URL(uiURl) - URL(apiURL) - URL(jitsiUrl) - val defaultWhitelist = ArrayList(PreferencesManager.getIntegrationWhiteListedUrl(context)) - defaultWhitelist.add(0, apiURL) - - val config = IntegrationManagerConfig( - uiUrl = uiURl, - apiUrl = apiURL, - jitsiUrl = jitsiUrl, - whiteListedUrls = defaultWhitelist) - WidgetsManager(config).also { + URL(sdkConfig.uiUrl) + URL(sdkConfig.apiUrl) + URL(sdkConfig.jitsiUrl) + + currentConfig = sdkConfig + + WidgetsManager(sdkConfig).also { this.widgetsManager = it } } catch (e: MalformedURLException) { diff --git a/vector/src/main/java/im/vector/widgets/WidgetsManager.java b/vector/src/main/java/im/vector/widgets/WidgetsManager.java index 174e892fac..70be4c0626 100755 --- a/vector/src/main/java/im/vector/widgets/WidgetsManager.java +++ b/vector/src/main/java/im/vector/widgets/WidgetsManager.java @@ -82,25 +82,6 @@ public String getUIUrl() { return config.getUiUrl(); } - /** - * Widget error code - */ - public class WidgetError extends MatrixError { - public static final String WIDGET_NOT_ENOUGH_POWER_ERROR_CODE = "WIDGET_NOT_ENOUGH_POWER_ERROR_CODE"; - public static final String WIDGET_CREATION_FAILED_ERROR_CODE = "WIDGET_CREATION_FAILED_ERROR_CODE"; - - /** - * Create a widget error - * - * @param code the error code (see XX_ERROR_CODE) - * @param detailedErrorDescription the detailed error description - */ - public WidgetError(String code, String detailedErrorDescription) { - errcode = code; - error = detailedErrorDescription; - } - } - /** * Pending widget creation callback @@ -114,10 +95,26 @@ public WidgetError(String code, String detailedErrorDescription) { * @param room the room to check. * @return the active widgets list */ - public List getActiveWidgets(MXSession session, Room room) { + public static List getActiveWidgets(MXSession session, Room room) { return getActiveWidgets(session, room, null, null); } + + public static Boolean isJitsiWidget(Widget widget) { + Event widgetEvent = widget.getWidgetEvent(); + if (widgetEvent == null) return false; + try { + JsonObject jsonObject = widgetEvent.getContentAsJsonObject(); + if (jsonObject != null && jsonObject.has("type")) { + String widgetType = jsonObject.get("type").getAsString(); + return WIDGET_TYPE_JITSI.equals(widgetType); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## getWidgets() failed : " + e.getMessage(), e); + } + return false; + } + /** * List all active widgets in a room. * @@ -127,7 +124,7 @@ public List getActiveWidgets(MXSession session, Room room) { * @param excludedTypes the excluded widget types * @return the active widgets list */ - private List getActiveWidgets(final MXSession session, final Room room, final Set widgetTypes, final Set excludedTypes) { + private static List getActiveWidgets(final MXSession session, final Room room, final Set widgetTypes, final Set excludedTypes) { // Get all im.vector.modular.widgets state events in the room List widgetEvents = room.getState().getStateEvents(new HashSet<>(Arrays.asList(WIDGET_EVENT_TYPE))); @@ -213,7 +210,7 @@ public int compare(Event e1, Event e2) { * @param room the room * @return the list of active widgets */ - public List getActiveJitsiWidgets(final MXSession session, final Room room) { + public static List getActiveJitsiWidgets(final MXSession session, final Room room) { return getActiveWidgets(session, room, new HashSet<>(Arrays.asList(WidgetsManager.WIDGET_TYPE_JITSI)), null); } @@ -224,7 +221,7 @@ public List getActiveJitsiWidgets(final MXSession session, final Room ro * @param room the room * @return the list of active widgets */ - public List getActiveWebviewWidgets(final MXSession session, final Room room) { + public static List getActiveWebviewWidgets(final MXSession session, final Room room) { return getActiveWidgets(session, room, null, new HashSet<>(Arrays.asList(WidgetsManager.WIDGET_TYPE_JITSI))); } @@ -236,7 +233,7 @@ public List getActiveWebviewWidgets(final MXSession session, final Room * @return an error if the user cannot act on widgets in this room. Else, null. */ - public WidgetError checkWidgetPermission(MXSession session, Room room) { + public static WidgetError checkWidgetPermission(MXSession session, Room room) { WidgetError error = null; if ((null != room) && (null != room.getState()) && (null != room.getState().getPowerLevels())) { @@ -489,7 +486,7 @@ public void onLiveEvent(MXSession session, Event event) { * @param callback the callback */ public void getFormattedWidgetUrl(final Context context, final Widget widget, final ApiCallback callback) { - if (isScalarUrl(context, widget.getUrl())) { + if (isScalarUrl(widget.getUrl())) { getScalarToken(context, Matrix.getInstance(context).getSession(widget.getSessionId()), new SimpleApiCallback(callback) { @Override public void onSuccess(String token) { @@ -509,11 +506,10 @@ public void onSuccess(String token) { /** * Return true if the url is allowed to receive the scalar token in parameter * - * @param context * @param url * @return true if the url is allowed to receive the scalar token in parameter */ - public boolean isScalarUrl(Context context, String url) { + public boolean isScalarUrl(String url) { List allowed = config.getWhiteListedUrls(); for (String allowedUrl : allowed) { if (url.startsWith(allowedUrl)) { diff --git a/vector/src/main/res/drawable-hdpi/ic_widget_bin.png b/vector/src/main/res/drawable-hdpi/ic_widget_bin.png new file mode 100644 index 0000000000..69d9ef5efa Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_widget_bin.png differ diff --git a/vector/src/main/res/drawable-hdpi/ic_widget_external_link.png b/vector/src/main/res/drawable-hdpi/ic_widget_external_link.png new file mode 100644 index 0000000000..9d966bc5fb Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_widget_external_link.png differ diff --git a/vector/src/main/res/drawable-hdpi/ic_widget_refresh.png b/vector/src/main/res/drawable-hdpi/ic_widget_refresh.png new file mode 100644 index 0000000000..1c9f1c69b1 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_widget_refresh.png differ diff --git a/vector/src/main/res/drawable-mdpi/ic_widget_bin.png b/vector/src/main/res/drawable-mdpi/ic_widget_bin.png new file mode 100644 index 0000000000..10fc50d87c Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ic_widget_bin.png differ diff --git a/vector/src/main/res/drawable-mdpi/ic_widget_external_link.png b/vector/src/main/res/drawable-mdpi/ic_widget_external_link.png new file mode 100644 index 0000000000..abeb028e24 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ic_widget_external_link.png differ diff --git a/vector/src/main/res/drawable-mdpi/ic_widget_refresh.png b/vector/src/main/res/drawable-mdpi/ic_widget_refresh.png new file mode 100644 index 0000000000..6f8d0dfd95 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ic_widget_refresh.png differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_widget_bin.png b/vector/src/main/res/drawable-xhdpi/ic_widget_bin.png new file mode 100644 index 0000000000..f3c9cb9188 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_widget_bin.png differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_widget_external_link.png b/vector/src/main/res/drawable-xhdpi/ic_widget_external_link.png new file mode 100644 index 0000000000..3e990b2891 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_widget_external_link.png differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_widget_refresh.png b/vector/src/main/res/drawable-xhdpi/ic_widget_refresh.png new file mode 100644 index 0000000000..4f5f4545f5 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_widget_refresh.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_widget_bin.png b/vector/src/main/res/drawable-xxhdpi/ic_widget_bin.png new file mode 100644 index 0000000000..a8d8947488 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_widget_bin.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_widget_external_link.png b/vector/src/main/res/drawable-xxhdpi/ic_widget_external_link.png new file mode 100644 index 0000000000..e5c2a2acf8 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_widget_external_link.png differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_widget_refresh.png b/vector/src/main/res/drawable-xxhdpi/ic_widget_refresh.png new file mode 100644 index 0000000000..561810122a Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_widget_refresh.png differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_widget_bin.png b/vector/src/main/res/drawable-xxxhdpi/ic_widget_bin.png new file mode 100644 index 0000000000..8cf7db66c5 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_widget_bin.png differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_widget_external_link.png b/vector/src/main/res/drawable-xxxhdpi/ic_widget_external_link.png new file mode 100644 index 0000000000..03b8c25b47 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_widget_external_link.png differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_widget_refresh.png b/vector/src/main/res/drawable-xxxhdpi/ic_widget_refresh.png new file mode 100644 index 0000000000..87d0743313 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_widget_refresh.png differ diff --git a/vector/src/main/res/layout/activity_widget.xml b/vector/src/main/res/layout/activity_widget.xml index b6fbce4536..0a4324712c 100755 --- a/vector/src/main/res/layout/activity_widget.xml +++ b/vector/src/main/res/layout/activity_widget.xml @@ -1,73 +1,20 @@ - + android:orientation="vertical"> - + android:layout_height="wrap_content" /> - - - - - - - - - - - - - + android:layout_height="match_parent" /> - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml new file mode 100644 index 0000000000..6aeb4c9d71 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + . Verwenden Sie die Alte-Überprüfung @@ -1656,4 +1656,56 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A app_id: Überprüfung + Keine + Widerrufen + Trennen + Kein Integrationsserver konfiguriert. + + Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen + Versuchen Sie es mit %s + Nicht erneut fragen + + Richten Sie eine E-Mail für die Kontowiederherstellung ein, die später von Personen, die Sie kennen, optional gefunden werden kann. + Richten Sie ein Telefon ein und lassen Sie es später optional von Personen erkennen, die Sie kennen. + Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. + Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. + Fallback-Call-Assist-Server zulassen + Optimiert für die Batterie + Optimiert für die Echtzeit + Keine Hintergrundsynchronisation + Fehler beim Aktualisieren der Einstellungen. + + + Bevorzugtes Synchronisationsintervall + Discovery + Öffentlicher Name (sichtbar für Personen, mit denen Sie kommunizieren) + Der öffentliche Name eines Geräts ist für Personen sichtbar, mit denen Sie kommunizieren + Um fortzufahren, müssen Sie die Bedingungen dieses Dienstes akzeptieren. + + Sie verwenden keinen Identity Server + Es ist kein Identitätsserver konfiguriert. Sie müssen Ihr Kennwort zurücksetzen. + + Sie versuchen anscheinend, eine Verbindung zu einem anderen Homeserver herzustellen. Möchten Sie sich abmelden\? + + push_key: + app_display_name: + Url: + Nutzungsbedingungen + Nutzungsbedingungen überprüfen + Für andere auffindbar sein + Verwenden Sie Bots, Bridges, Widgets und Sticker-Packs + + Lesen Sie bei + + + Identitätsserver + Trennen Sie den Identitätsserver + Konfigurieren Sie den Identitätsserver + Identitätsserver ändern + Erkennbare E-Mail-Adressen + Erkennungsoptionen werden angezeigt, sobald Sie eine E-Mail hinzugefügt haben. + ausstehend + + Gib einen neuen Identitätsserver ein + Konnte keine Verbindung zum Heimserver herstellen. diff --git a/vector/src/main/res/values-el/strings.xml b/vector/src/main/res/values-el/strings.xml index 6e4706bc63..4b70066fe4 100644 --- a/vector/src/main/res/values-el/strings.xml +++ b/vector/src/main/res/values-el/strings.xml @@ -3,7 +3,7 @@ el - ΕL + GR Ακύρωση diff --git a/vector/src/main/res/values-es-rMX/strings.xml b/vector/src/main/res/values-es-rMX/strings.xml index 423ecb5438..313e7737d1 100644 --- a/vector/src/main/res/values-es-rMX/strings.xml +++ b/vector/src/main/res/values-es-rMX/strings.xml @@ -779,4 +779,9 @@ Dispositivos desconocidos: Enviar una respuesta cifrada… Enviar una respuesta (sin cifrar)… + Latn + + Iniciando servicio + Copia de seguridad de la clave + Usar copia de seguridad de la clave diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 894f7e3d47..252acefabc 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1420,8 +1420,8 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Firma autocompletar opciones del servidor - Riot ha detectado una configuración personalizada del servidor para el dominio de su ID de usuario \"%s\": -\n%s + Riot ha detectado una configuración personalizada del servidor para el dominio de su ID de usuario \"%1$s\": +\n%2$s Configuración de uso Origen predeterminado de medios diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 598b8907cc..57eed05cca 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -414,7 +414,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Bigarren planoko sinkronizazioa Gaitu bigarren planoko sinkronizazioa Sinkronizazio eskaerak debora-muga gainditu du - Eskaeren arteko itxaronaldia + Sinkronizazioen arteko itxaronaldia segundo segundo @@ -441,10 +441,10 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Finkatu irakurri gabeko mezuak dituzten gelak Gailuak Erakutsi mezu guztien denbora-zigilua - Gailuaren xehetasunak + Gailuaren informazioa ID - Izena - Gailuaren izena + Izen publikoa + Aldatu izen publikoa Azkenekoz ikusia %1$s @ %2$s Eragiketa honek autentifikazio gehigarria behar du. @@ -574,9 +574,9 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Deszifratze errorea Igorlearen gailuaren informazioa - Gailuaren izena - Izena - Gailuaren IDa + Izen publikoa + Izen publikoa + IDa Gailuaren gakoa Egiaztaketa Ed25519 hatz-marka @@ -870,7 +870,7 @@ Baten bat gehitu orain? Desaktibatu kontua %1$s hasiera-zerbitzaria erabiltzen jarraitzeko erabilera baldintzak irakurri eta onartu behar dituzu. - Irakurri orain + Berrikusi orain Desaktibatu kontua Honek kontua behin betirako erabilgaitza bihurtuko du. Ezin izango duzu saioa hasi, eta ezin izango du beste inork ID hori erabili. Kontua dagoen gela guztietatik aterako da, eta kontuaren xehetasunak identitate-zerbitzaritik ezabatuko dira. Ekintza hau ezin da desegin. @@ -1048,7 +1048,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Onartu - Berrikusi eta onartu hasiera-zerbitzari honen politikak: + Irakurri eta onartu hasiera-zerbitzari honen baldintzak: %1$d/%2$d gako ongi inportatu dira. @@ -1564,7 +1564,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. \n \nRiotX bezeroak honakoa ahalbidetzen du: • Badagoen kontu batean saioa hasi • Gelak sortu eta gela publikoetara elkartu • Gonbidapenak onartu edo ukatu • Erabiltzailearen gelak zerrendatu • Gelaren xehetasunak ikusi • Testuzko mezuak bidali • Eranskinak bidali • Zifratutako geletan mezuak irakurri eta idatzi • Zifratzea: E2Egakoen babeskopia, gailuaren egiaztaketa aurreratua, gakoa partekatzeko eskaria eta erantzuna • Push jakinarazpena • Gai argia, iluna eta beltza \n -\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Txat zuzenerako gelak sortzea • Deiak • Trepetak • … +\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Deiak • Trepetak • … app_display_name: Mezu zuzenak @@ -1612,4 +1612,158 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Ikusi edizioen historiala + Bat ere ez + Indargabetu + Deskonektatu + Berrikusi + Ukatu + + Ez da identitate-zerbitzaririk konfiguratu. + + Deiak huts egin du zerbitzaria gaizki konfiguratuta dagoelako + Eskatu zure hasiera-zerbitzariko administratzaileari (%1$s) TURN zerbitzari bat konfiguratu dezala deiak ondo funtzionatu dezaten. +\n +\nBestela, %2$s zerbitzari publikoa erabili dezakezu, baina hau ez da hain fidagarria izango, eta zure IP-a partekatuko du zerbitzari horrekin. Hau ezarpenetan aldatu dezakezu. + Saiatu %s erabiltzen + Ez galdetu berriro + + Ezarri E-mail bat kontua berreskuratzeko, eta gero aukeran zure ezagunek aurkitu zaitzaten. + Ezarri telefono bat gero aukeran zure ezagunek aurkitu zaitzaten. + Ezarri E-mail bat kontua berreskuratzeko. Erabili geroo aukeran e-maila edo telefonoa zure ezagunek aurkitu zaitzaten. + Ezarri E-mail bat kontua berreskuratzeko. Erabili geroo aukeran e-maila edo telefonoa zure ezagunek aurkitu zaitzaten. + Ezin izan da hasiera-zerbitzari bat atzitu URL honetan, egiaztatu ezazu + Baimendu lehenetsitako deien laguntzarako zerbitzaria + %s erabiliko da laguntzarako zure hasiera-zerbitzariak ez badu bat eskaintzen (Zure IP helbidea deian partekatuko da) + Gehitu identitate-zerbitzari bat zure ezarpenetan ekintza hau burutzeko. + Bigarren planoko sinkronizazio modua (Esperimentala) + Bateria erabilerarako optimizatua + Riot bigarren planoan sinkronizatuko da gailuaren baliabide mugatuen erabilera ahal beste murriztuz (bateria). +\nZure gailuaren baliabideen egoeraren arabera, sistema eragileak sinkronizazioa atzeratu dezake. + Denbora errealerako optimizatua + Riot bigarren planoan sinkronizatuko da maiztasun finkoarekin (konfiguragarria). +\nHonek irrati eta bateriaren erabileran eragina izango du, eta Riot gertaerei adi dagoela dion jakinarazpen bat bistaratuko da etengabe. + Ez sinkronizatu bigarren planoan + Ez zaizu jasotako mezuei buruz jakinaraziko aplikazioa bigarren planoan dagoenean. + Huts egin du ezarpenak eguneratzean. + + + Hobetsitako sinkronizazio tartea + %s +\nSinkronizazioa atzeratu daiteke zure baliabideen arabera (bateria) edo gailuaren egoeraren arabera (lo). + Aurkitzea + Kudeatu aurkitzeko ezarpenak. + Izen publikoa (Zurekin komunikatzen den jendeak ikusi dezake) + Gailu baten izen publikoa zurekin komunikatzen den jendeak ikusi dezake + Jarraitzeko erabilera baldintzak onartu behar dituzu. + + Ez duzu identitate zerbitzaririk erabiltzen + Ez da identitate zerbitzaririk konfiguratu, zure pasahitza berrezartzeko beharrezkoa da. + + Riot bertsio zaharrek segurtasun akats bat zuten eta zure identitate zerbitzariak (%1$s) zure kontua atzitu zezakeen. %2$s fidagarritzat jotzen baduzu hau ezikusi dezakezu, bestela maiatu saioa eta hasi berriro. +\n +\nXehetasun gehiago hemen: +\nhttps://medium.com/@RiotChat/36b4792ea0d6 + + Badirudi beste hasiera-zerbitzari batera konektatzen saiatzen ari zarela. Saioa amaitu nahi duzu\? + + Erabilera baldintzak + Irakurri baldintzak + Izan besteentzat aurkigarria + Erabili botak, zubiak, trepetak eta eranskailu multzoak + + Irakurri hemen + + + Identitate-zerbitzaria + Deskonektatu identitate-zerbitzaria + Konfiguratu identitate-zerbitzaria + Aldatu identitate-zerbitzaria + Orain %1$s erabiltzen ari zara ezagunak aurkitzeko eta ezagunek zu aurkitzeko. + Orain ez duzu identitate-zerbitzaririk erabiltzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian. + E-mail helbide aurkigarria + Aurkitze aukerak behin e-mail bat gehitu duzunean agertuko dira. + Aurkitze aukerak behin telefono zenbaki bat gehitu duzunean agertuko dira. + Identitate-zerbitzaritik deskonektatzean beste erabiltzaileek ezin izango zaituzte e-mail edo telefonoa erabilita aurkitu, eta zuk ezin izango dituzu e-mail edo telefonoa erabilita aurkitu. + Telefono zenbaki aurkigarriak + Berrespen e-mail bat bidali dizugu %s helbidera, begiratu zure e-maila eta sakatu baieztapen esteka + Egiteke + + Sartu identitate-zerbitzari berria + Ezin izan da identitate-zerbitzarira konektatu + Sartu identitate-zerbitzariaren URL-a + Identitate-zerbitzariak ez du erabilera baldintzarik + Hautatu duzun identitate-zerbitzariak ez du erabilera baldintzarik. Jarraitu soilik zerbitzuaren jabea fidagarritzat jotzen baduzu + SMS mezu bat bidali zaizu %s zenbakira. Sartu hemen mezu horrek daukan egiaztatze-kodea. + + Orain e-mail helbideak edo telefono zenbakiak partekatzen dituzu %1$s zerbitzarian. %2$s zerbitzarira konektatu beharko zara partekatzeari uzteko. + Onartu %s identitate-zerbitzariaren erabilera baldintzak besteek zu e-mail helbidea edo telefonoa erabiliz aurkitzea ahalbidetzeko. + Latn + + Gaitu egunkari xehetsuak. + Amorruz astintzean egunkari xehetsuak bidaltzeak garatzaileei laguntzen diete. Gaituta badago ere, aplikazioak ez ditu mezuen edukiak edo beste datu probaturik gordetzen egunkarian. + + + Saiatu berriro zure hasiera-zerbitzariaren erabilera baldintzak onartu eta gero. + + Badirudi zerbitzariak luze hartu duela erantzuteko, hau konexio kaxkar baten ondorioz izan daiteke edo zerbitzarian errore bat dagoelako. Saiatu berriro geroago. + + Bidali eranskina + + Ireki nabigazio-tiradera + Ireki gela sortzeko menua + Itxi gela sortzeko menua… + Sortu elkarrizketa zuzen berria + Sortu gela berria + Itxi gakoen babes-kopiaren banda + Erakutsi pasahitza + Ezkutatu pasahitza + Jauzi behera + + %1$s, %2$s eta beste %3$d erabiltzailek irakurria + %1$s, %2$s eta%3$s erabiltzaileek irakurria + %1$s eta %2$s erabiltzaileek irakurria + %s erabiltzaileak irakurria + + Erabiltzaile batek irakurria + %d erabiltzailek irakurria + + + \'%1$s\' fitxategia (%2$s) handiegia da igotzeko. Muga %3$s da. + + Errore bat gertatu da eranskina eskuratzean. + Fitxategia + Kontaktua + Kamera + Audioa + Galeria + Eranskailua + Ezin izan dira partekatutako datuak kudeatu + + Spama da + Desegokia da + Salaketa pertsonalizatua + Salatu eduki hau + Eduki hau salatzeko arrazoia + SALATU + BLOKEATU ERABILTZAILEA + + Edukia salatuta + Eduki hau salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + Spam gisa salatua + Eduki hau spam gisa salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + Desegoki gisa salatua + Eduki hau desegoki gisa salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + + Riot-ek zure E2E gakoak diskoan gordetzeko baimena behar du. +\n +\nBaimendu sarbidea hurrengo laster-leihoan zure gakoak eskuz esportatu ahal izateko. + + Ez dago sare konexiorik orain + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 3eb5e6ff9d..651978f92f 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -6,7 +6,7 @@ FI - Keskustelut + Viestit Huone Asetukset Jäsenen tiedot @@ -155,7 +155,7 @@ Toista salasana Vahvista uusi salasanasi Väärä käyttäjätunnus ja/tai salasana - Käyttäjätunnus saa koostua vain kirjaimista a-z, numeroista, pisteistä, väliviivoista ja alaviivoista + Käyttäjätunnus saa koostua vain kirjaimista a-z, numeroista, pisteistä, yhdysviivoista ja alaviivoista Liian lyhyt salasana (vähintään 6 merkkiä) Salasana puuttuu Tämä ei näytä oikealta sähköpostiosoitteelta @@ -175,15 +175,15 @@ Kotipalvelin: Identiteettipalvelin: Olen varmistanut sähköpostiosoitteeni - Nollataksesi salasanasi, anna tilisi sähköpostiosoite: - Anna tilisi sähköpostiosoite. + Palauttaaksesi salasanasi, anna tiliisi liitetty sähköpostiosoite: + Anna tiliisi liitetty sähköpostiosoite. Anna uusi salasana. Osoitteeseen %s on lähetetty sähköposti. Kun olet avannut siinä olevan linkin, paina alla olevaa nappia. Sähköpostiosoitteesi vahvistaminen epäonnistui. Varmista, että klikkasit sähköpostissa olevaa linkkiä Salasanasi on vaihdettu.\n\nSinut on kirjauduttu ulos kaikista laitteistasi, etkä enää saa viesti-ilmoituksia. Ottaaksesi käyttöön ilmoitukset uudelleen, kirjaudu sisään uudelleen kaikilla laitteillasi. - URL:n on alettava seuraavasti: http[s]:// + URL-osoitteen on alettava seuraavasti: http[s]:// Kirjautuminen epäonnistui: Verkkovirhe Kirjautuminen epäonnistui Rekisteröityminen epäonnistui: Verkkovirhe @@ -232,7 +232,7 @@ Yhdistetty Yhdistetään… - Puhelu loppu + Puhelu loppui Soitetaan… Saapuva puhelu Saapuva videopuhelu @@ -285,7 +285,7 @@ Hylkää - Hyppää ensimmäiseen lukemattomaan viestiin. + Siirry ensimmäiseen lukemattomaan viestiin. %s on kutsunut sinut huoneeseen @@ -298,7 +298,7 @@ Uusi keskustelu Lisää jäsen - 1 jäsen + yksi jäsen Poistu huoneesta @@ -349,7 +349,7 @@ Lähetä salattu viesti… Lähetä viesti (salaamaton)… Yhteys palvelimeen katkesi. - Viesteja ei lähetetty. %1$s vai %2$s\? + Viestejä ei lähetetty. %1$s vai %2$s\? Viestejä ei lähetetty koska huoneessa on tuntemattomia laitteita. %1$s vai %2$s\? Lähetä kaikki uudelleen Peruuta kaikki @@ -423,7 +423,7 @@ Unohda - Keskustelut + Viestit Asetukset Versio Käyttöehdot @@ -785,9 +785,9 @@ Yhteisöt Ei ryhmiä - Oletko varma, että haluat aloittaa uuden keskustelun käyttäjän %s kanssa\? - Oletko varma että haluat aloittaa äänipuhelun? - Oletko varma, että haluat aloittaa videopuhelun? + Haluatko varmasti aloittaa uuden keskustelun käyttäjän %s kanssa\? + Haluatko varmasti aloittaa äänipuhelun\? + Haluatko varmasti aloittaa videopuhelun\? Ryhmälistaus @@ -933,12 +933,12 @@ Haluatko lisätä paketteja? Avaa otsikko Synkronoidaan… - yksi aktiivinen käyttäjä - %d aktiivista käyttäjää + yksi aktiivinen jäsen + %d aktiivista jäsentä - yksi käyttäjä - %d käyttäjää + yksi jäsen + %d jäsentä 1 s @@ -1136,12 +1136,12 @@ Haluatko lisätä paketteja? Kirjoita tähän… - yksi lukematon viesti - %d lukematonta viestiä + yksi lukematon ilmoitettu viesti + %d lukematonta ilmoitettua viestiä - 1 lukematon viesti - %d lukematonta viestiä + yksi lukematon ilmoitettu viesti + %d lukematonta ilmoitettua viestiä yksi huone @@ -1156,7 +1156,7 @@ Haluatko lisätä paketteja? Parametri ei ole kelvollinen. Käynnistä järjestelmän kamera Riotin kameraruudun sijaan. Käytä näppäimistön rivinvaihtopainiketta viestin lähettämiseen - Tämä vaihtoehto vaatii kolmannen osapuolen sovelluksen viestien tallennukseen. + Tämä valinta vaatii kolmannen osapuolen sovelluksen viestien tallennukseen. Komento ”%s” vaatii enemmän parametreja, tai jotkin parametrit ovat virheellisiä. Näyttää toiminnon @@ -1190,17 +1190,17 @@ Haluatko lisätä paketteja? Jatkaaksesi kotipalvelimen %1$s käyttöä, sinun täytyy hyväksyä palvelun käyttöehdot. Näytä ehdot - Tämä tekee tunnuksestasi lopullisesti käyttökelvottoman. Et voi kirjautua sisään eikä kukaan pysty rekisteröimään tunnusta samalla käyttäjä-ID:llä. Tämä saa tunnuksesi lähtemään kaikista huoneista, joissa se on osallisena, ja se poistaa tunnuksen tiedot identiteettipalvelimelta. Tämä toiminto on peruuttamaton. + Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään eikä kukaan pysty rekisteröitymään samalla käyttäjätunnuksella. Tämä saa tilisi poistumaan kaikista huoneista, joissa se on osallisena, ja poistaa tilin tiedot identiteettipalvelimelta. Tämä toiminto on peruuttamaton. \n -\nTunnuksen otto pois käytöstä ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä. Jos haluat meidän unohtavan viestisi, merkitse alapuolella oleva laatikko. +\nTilin deaktivointi ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä. Jos haluat meidän unohtavan viestisi, merkitse alapuolella oleva valintaruutu. \n -\nViestin näkyvyys Matrixissa on samanlainen kuin sähköpostissa. Vaikka unohdamme viestisi, kaikki tahot, joilla viestisi jo on, tulevat pääsemään omaan kopioonsa viesteistäsi. - Unohda kaikki viestit, jotka olen lähittänyt, kun tunnuksesi on deaktivoitu (varoitus: tämä aiheuttaa tulevien käyttäjien näkevän vanhat keskustelusi epätäydellisinä) +\nViestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Viestiesi unohtaminen tarkoittaa, että lähettämiäsi viestejä ei näytetä uusille tai rekisteröitymättömille käyttäjille. Ne rekisteröityneet käyttäjät, joilla viestisi jo on, pääsevät kuitenkin näkemään oman kopionsa niistä jatkossakin. + Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on deaktivoitu (Varoitus: tästä seuraa, että tulevat käyttäjät näkevät vanhat keskustelut epätäydellisinä) Syötä käyttäjätunnus. Tämä huone on korvattu toisella huoneella Keskustelu jatkuu täällä Tämä huone on jatkoa toiselle keskustelulle - Täppää tästä nähdäksesi vanhat viestit + Paina tästä nähdäksesi vanhemmat viestit Resurssiraja saavutettu Ota yhteys ylläpitäjään @@ -1227,7 +1227,7 @@ Haluatko lisätä paketteja? Näytä infoalue Aina - Tiedotuksille ja virheille + Viesteille ja virheille Vain virheille %1$s: @@ -1249,7 +1249,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Poista salalause, jos haluat Riotin generoivan palautusavaimen. Matrix-istuntoa ei ole saatavilla - Älä ikinä menetä salattuja viestejä + Älä koskaan menetä salattuja viestejä Salatuissa huoneissa viestit ovat suojattuna osapuolten välisellä salauksella. Vain sinä ja vastaanottaja(t) omistavat avaimet näiden viestien lukemiseen. \n \nVarmuuskopioi avaimesi, jotta et menetä niitä. @@ -1258,9 +1258,9 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Tallenna avaimet käsin Turvaa varmuuskopiosi salalauseella. - Tallennamme salatun kopion avaimistasi kotipalvelimellesi. Suojaa varmuuskopiosi salalauseella pitääksesi sen turvattuna. -\n -\nTurvallisuuden takia tämän salalauseen tulisi olla eri tunnuksesi salasanasta. + Tallennamme salatun kopion avaimistasi kotipalvelimellesi. Suojaa varmuuskopiosi salalauseella pitääksesi sen turvattuna. +\n +\nParhaan turvallisuuden takaamiseksi salalauseen tulisi olla eri kuin tilisi salasana. Aseta salalause Luodaan varmuuskopiota Tai, turvaa varmuuskopio palautusavaimella, tallentamalla palautusavain johonkin turvalliseen paikkaan. @@ -1377,7 +1377,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Kirjaudu sisään kertakirjautumisella Tämä osoite ei ole saavutettavissa. Tarkistathan osoitteen - Laitteesi käyttää vanhentunutta, haavoittuvaista TLS-protokollan versiota. Turvallisuutesi vuoksi et pysty yhdistämään + Laitteesi käyttää vanhentunutta, haavoittuvaista TLS-protokollan versiota. Turvallisuutesi tähden et voi muodostaa yhteyttä Lähetä viesti enter-näppäimellä Näppäimistön enter-näppäin lähettää viestin sen sijaan, että se lisäisi rivinvaihdon @@ -1387,25 +1387,25 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös %1$s -> %2$s - Keskusteluohjelma, joka on täysin hallinnoitavissasi ja erittäin joustava. Riot sallii sinun kommunikoivan juuri sillä tavalla kuin haluat. [matrix]-sovellus — avoimen ja hajautetun kommunikaation standardi. + Keskustelusovellus, joka on sinun hallinnassasi ja erittäin joustava. Riot antaa sinun viestiä juuri sillä tavalla kuin haluat. Taustalla [matrix] – avoimen ja hajautetun viestinnän standardi. \n -\nKäytä ilmaista matrix.org-tunnusta, hanki oma palvelimesi osoitteesta https://modular.im tai käytä muuta Matrix-palvelinta. +\nHanki ilmainen matrix.org-tili, hanki oma palvelimesi osoitteesta https://modular.im tai käytä muuta Matrix-palvelinta. \n \nMiksi valita Riot.im\? \n -\n• Täydellinen kommunikaatio: rakenna huoneita tiimeillesi, ystävillesi ja yhteisöllesi — juuri niin kuin haluat! Keskustele, jaa tiedostoja, lisää sovelmia ja tee ääni‐ ja videopuheluita — kaikki tämä ilmaiseksi. -\n -\n• Tehokkaat integraatiot: Käytä Riotia juuri niillä työkaluilla, jotka tiedät ja joita rakastat. Riot.im jopa mahdollistaa keskustelun henkilöiden kanssa, jotka käyttävät eri keskusteluohjelmia. +\n• Kattavat mahdollisuudet viestintään: rakenna huoneita tiimeillesi, ystävillesi ja yhteisöllesi – juuri niin kuin haluat! Keskustele, jaa tiedostoja, lisää sovelmia ja soita ääni‐ ja videopuheluita – kaikki tämä ilmaiseksi. +\n +\n• Tehokkaat integraatiot: Käytä Riotia tuntemiesi työkalujen kanssa. Riot.im mahdollistaa keskustelut jopa eri keskusteluohjelmia käyttävien ihmisten ja ryhmien kanssa. \n -\n• Yksityinen ja turvallinen: Pidä keskustelusi salaisina. Nykyaikainen osapuolten välinen salaus pitää huolen, että yksityiset keskustelut pysyvät yksityisenä. +\n• Yksityinen ja turvallinen: Pidä keskustelusi salaisina. Nykyaikainen osapuolten välinen salaus pitää huolen, että yksityiset keskustelut pysyvät yksityisinä. \n -\n• Avoin, ei suljettu: Avointa koodia, ja rakennettu käyttämään Matrixia. Voit omistaa oman datasi ylläpitämällä omaa palvelintasi, tai käyttämällä palvelinta, johon luotat. +\n• Avoin, ei suljettu: Avointa lähdekoodia ja rakennettu käyttämään Matrixia. Voit omistaa oman datasi ylläpitämällä omaa palvelintasi, tai käyttämällä palvelinta, johon luotat. \n -\n• Kaikkialla, missä olet: pysy yhteydessä siellä, missä ikinä oletkin täysin synkronoidulla viestihistorialla kaikkien laitteidesi ja sivun https://riot.im välillä. +\n• Kaikkialla, missä olet: pysy yhteydessä missä ikinä oletkin, täysin synkronoidulla viestihistorialla kaikkien laitteidesi ja https://riot.im-verkkopalvelun välillä. - Uusi avainvarmuuskopio on löydetty. + Uusi avainvarmuuskopio löydetty. \n -\nJos et asettanut uutta palautustapaa, hyökkääjä saattaa yrittää päästä käsiksi tunnukseesi. Vaihda tunnuksesi salasana ja aseta uusi palautustapa asetuksissa välittömästi. +\nJos et asettanut uutta palautustapaa, hyökkääjä saattaa yrittää päästä käsiksi tiliisi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi. Epäkelpo kotipalvelimen löytövastaus Automaattitäydennyksen palvelinasetukset Riot löysi mukautetun palvelinasetuksen userId:si domainille ”%1$s”: @@ -1604,7 +1604,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Play Storen kuvaus Push-säännöt %1$s luodaksesi tilin. -Katkaise yhteys + Katkaise yhteys Kieltäydy Identiteettipalvelinta ei ole määritetty. @@ -1646,7 +1646,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Katkaise yhteys identiteettipalvelimeen Määritä identiteettipalvelin Vaihda identiteettipalvelinta - Käytät palvelinta %1$s löytääksesi tuntemiasi ihmisiä ja jotta he löytäisivät sinut. + Käytät palvelinta %1$s löytääksesi tuntemiasi ihmisiä ja ollaksesi heidän löydettävissään. Et käytä tällä hetkellä identiteettipalvelinta. Jotta voit löytää tuntemiasi ihmisiä ja jotta he löytävät sinut, määritä identiteettipalvelin alla. Odottaa @@ -1655,6 +1655,56 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Syötä identiteettipalvelimen URL-osoite Identiteettipalvelimella ei ole käyttöehtoja Valitsemallasi identiteettipalvelimella ei ole käyttöehtoja. Jatka vain, jos luotat palvelun omistajaan - Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %s. Sinun täytyy yhdistää uudelleen palvelimeen %s, jotta voit lopettaa niiden jakamisen. + Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %1$s. Sinun täytyy yhdistää uudelleen palvelimeen %2$s, jotta voit lopettaa niiden jakamisen. Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella. + Aseta sähköposti tilin palauttamista varten. Myöhemmin voit halutessasi antaa ihmisten etsiä sinua sen perusteella. + Salli varalla oleva puhelun apupalvelin + Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät voi etsiä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella. + Lähetimme sinulle vahvistussähköpostin osoitteeseen %s, tarkista sähköpostisi ja klikkaa vahvistuslinkkiä + Ota yksityiskohtaiset lokit käyttöön. + Yritä uudelleen, kun olet hyväksynyt kotipalvelimesi käyttöehdot. + + Palvelimen vastaus näyttää viipyvän. Tämä voi johtua kehnosta yhteydestä tai palvelimillamme tapahtuneesta virheestä. Yritä hetken kuluttua uudelleen. + + Lähetä liite + + Luo uusi huone + Näytä salasana + Piilota salasana + Siirry loppuun + + %1$s, %2$s ja %3$d muuta lukivat + %1$s, %2$s ja %3$s lukivat + %1$s ja %2$s lukivat + %s luki + + 1 käyttäjä luki + %d käyttäjää luki + + + Liitettä noudettaessa tapahtui virhe. + Tiedosto + Kamera + Galleria + Tarra + Se on roskapostia + Se on sopimaton + Verkkoyhteyttä ei ole juuri nyt + + Ei mitään + Pyydä kotipalvelimesi (%1$s) ylläpitäjää määrittämään TURN-palvelin, jotta puhelut toimivat luotettavasti. +\n +\nVaihtoehtoisesti voit yrittää käyttää julkista palvelinta osoitteessa %2$s, mutta tämä ei ole yhtä luotettava vaihtoehto ja antaa IP-osoitteesi kyseisen palvelimen tietoon. Voit myös hallita tätä asetuksista. + Kokeile käyttää palvelinta %s + Käyttää palvelinta %s apupalvelimena, jos kotipalvelimesi ei tarjoa sellaista (IP-osoitteesi näkyy palvelimelle puhelun aikana) + Optimoitu akunkestoa varten + Riot synkronoi taustalla laitteen rajallisia resursseja (akkua) säästäen. +\nLaitteesi resurssien tilasta riippuen käyttöjärjestelmä saattaa lykätä synkronointia. + Optimoitu reaaliaikaa varten + Riot synkronoi taustalla täsmällisin aikavälein (säädettävä). +\nTämä vaikuttaa radion ja akun käyttöön. Näet pysyvän ilmoituksen, joka kertoo, että Riot kuuntelee tapahtumia. + Viestimuokkaukset + Ole löydettävissä + Tekstiviesti on lähetetty numeroon %s. Syötä sen sisältämä varmistuskoodi. + diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 6e3841867e..6057627163 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -783,7 +783,7 @@ Appareils inconnus : Abandonner le salon Badge - Secouer avec frustration pour signaler un bug + Secouer avec frustration pour signaler une anomalie Actions Synchronisation… @@ -1570,7 +1570,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq \n \nRiotX prend en charge : • Se connecter à un compte existant • Créer de salons et rejoindre des salons publics • Accepter et refuser des invitations • Lister les salons des utilisateurs • Voir les informations des salons • Envoyer des messages texte • Envoyer des pièces jointes • Lire et écrire des messages dans les salons chiffrés • Chiffrement : sauvegarde des clés de chiffrement, vérification avancée des appareils, demande et réponse de partage de clé • Notifications • Thèmes clair, sombre et noir \n -\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Création de salons de discussion directe • Appels • Widgets • … +\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Appels • Widgets • … Messages directs @@ -1634,7 +1634,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Lu à -Aucun + Aucun Révoquer Déconnecter Aucun serveur d’identité configuré. @@ -1699,6 +1699,75 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Le serveur d’identité qui vous avez choisi n’a pas de conditions de service. Continuez uniquement si vous faites confiance au propriétaire de ce service Un SMS a été envoyé à %s. Saisissez le code de vérification qu’il contient. - Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %s. Vous devrez vous reconnecter à %s pour arrêter de les partager. + Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %1$s. Vous devrez vous reconnecter à %2$s pour arrêter de les partager. Acceptez les conditions de service du serveur d’identité (%s) pour vous permettre d’être découvrable avec une adresse e-mail ou un numéro de téléphone. + Latn + + Activer les journaux verbeux. + Les journaux verbeux aideront les développeurs en fournissant plus de journaux quand vous envoyez un rapport d’anomalie. Même si cette option est activée, l’application n’envoie pas le contenu des messages ou toute autre donnée personnelle. + + + Réessayez quand vous aurez accepté les termes et conditions de votre serveur d’accueil. + + On dirait que le serveur mette trop de temps à répondre. Ça peut être dû à une mauvaise connexion ou à une erreur avec nos serveurs. Réessayez plus tard. + + Envoyer une pièce jointe + + Ouvrir le menu de navigation + Ouvrir le menu de création de salon + Fermer le menu de création de salon… + Créer une nouvelle conversation directe + Créer un nouveau salon + Fermer la bannière de sauvegarde des clés + Afficher le mot de passe + Masquer le mot de passe + Sauter en bas de page + + %1$s, %2$s et %3$d autres ont lu + %1$s, %2$s et %3$s ont lu + %1$s et %2$s ont lu + %s a lu + + 1 utilisateur a lu + %d utilisateurs ont lu + + + Le fichier « %1$s » (%2$s) est trop gros pour être envoyé. La limite est %3$s. + + Une erreur est survenue pendant la récupération de la pièce jointe. + Fichier + Contact + Appareil photo + Audio + Galerie + Sticker + Impossible de traiter les données de partage + + C’est du pourriel + C’est inapproprié + Signalement personnalisé + Signaler ce contenu + Motif de signalement de ce contenu + SIGNALER + BLOQUER L’UTILISATEUR + + Contenu signalé + Ce contenu a été signalé. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + Signalé comme pourriel + Ce contenu a été signalé comme pourriel. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + Signalé comme inapproprié + Ce contenu a été signalé comme inapproprié. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + + Riot a besoin de votre permission pour sauvegarder vos clés de chiffrement sur le disque. +\n +\nAutorisez l’accès dans le prochaine fenêtre pour pouvoir exporter vos clés manuellement. + + Il n’y a aucune connexion au réseau pour le moment + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index a3d72189ba..caca6bd7c8 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1021,7 +1021,7 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe.Elküld Ezeket az embereket biztosan kirúgod? - + Ok @@ -1249,11 +1249,11 @@ Figyelmeztetés: ez a fájl törlésre kerülhet, ha az alkalmazást törlik.Visszaállítva: %1$d kapcsolati kulcs, és %2$d kulcs, ami(k) ismeretlenek voltak az eszköz számára, hozzáadva Visszaállított mentés %d kulccsal. - + %d új kulcs lett hozzáadva ehhez az eszközhöz. - + "Nem sikerült beszerezni a legfrissebb verziójú visszaállítási kulcsot (%s)." @@ -1332,7 +1332,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Minden kulcs elmentve %d kulcs mentése… - + Verzió @@ -1569,7 +1569,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró \n \nRiotX ezeket támogatja: • Bejelentkezés létező fiókba • Szoba készítés és nyilvános szobába való belépés • Meghívók fogadása és elutasítás • Felhasználók szobáinak listázása • Szoba adatainak megtekintése • Szöveges üzenet küldése • Csatolmány küldése • Titkosított szobákban üzenetek olvasása és írása • Titkosítás: Végponttól végpontig titkosító kulcsok mentése, fejlett eszköz ellenőrzés, kulcs megosztás kérése és válasz • „Push” értesítések • Világos, sötét és fekete téma \n -\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Közvetlen beszélgetések indítása • Hívások • Kisalkalmazások • … +\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Hívások • Kisalkalmazások • … Közvetlen beszélgetés @@ -1633,7 +1633,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Olvasd itt -Nincs + Nincs Visszavon Lecsatlakozik Azonosítási szerver nincs beállítva. @@ -1698,6 +1698,75 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Az általad választott azonosítási szervernek nincs felhasználási feltétele. Csak akkor lépj tovább ha megbízol a szolgáltatás tulajdonosában Szöveges üzenetet küldtünk ide: %s. Kérlek add meg az ellenőrző kódot amit az üzenet tartalmaz. - Az azonosítási szerverrel (%s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %s, hogy megállítsd a megosztást. + Az azonosítási szerverrel (%1$s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %2$s, hogy megállítsd a megosztást. Egyetértek az azonosítási szerver (%s) Felhasználási feltételeivel ahhoz, hogy megtalálható legyek e-mail címmel vagy telefonszámmal. + Latn + + Kibővített naplózás engedélyezése. + A kiterjesztett naplózás a fejlesztőknek nyújt több információt amikor hibajegyet küldesz. Még bekapcsolva sem naplóz üzenet tartalmat vagy más személyes adatot. + + + Kérlek ismételd meg miután elfogadtad a matrix szervered felhasználási feltételeit. + + Úgy tűnik a szerver sokáig nem válaszol, ennek a bizonytalan kapcsolat vagy egy hiba a szerverünkben lehet az oka. Kicsit később próbáld újra. + + Csatolmány küldése + + Navigációs panel megnyitása + Szoba készítés menü megnyitása + Szoba készítés menü bezárása… + Új közvetlen beszélgetés indítása + Új szoba készítése + Kulcs mentés csík bezárása + Jelszó mutatása + Jelszó elrejtése + Végére ugrás + + %1$s, %2$s és %3$d olvasták + %1$s, %2$s és %3$s olvasták + %1$s és %2$s olvasták + %s olvasta + + 1 felhasználó olvasta + %d felhasználó olvasta + + + \'%1$s\' (%2$s) fájl túl nagy a feltöltéshez. A korlát: %3$s. + + A csatolmány letöltésénél hiba történt. + Fájl + Kapcsolat + Kamera + Hang + Galéria + Matrica + Az adatmegosztást nem sikerül kezelni + + Ez nemkívánt (spam) + Ez nem idevaló + Egyedi jelentés + Tartalom bejelentése + A tartalom bejelentésének oka + JELENTÉS + FELHASZNÁLÓ BLOKKOLÁSA + + Tartalom bejelentve + Ez a tartalom bejelentve. +\n +\nHa nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + Bejelentve nem kívántként (spam) + Ez a tartalom nem kívántnak (spam) lett bejelentve. +\n +\n Ha nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + Nem idevalónak bejelentve + Ez a tartalom nem idevalónak lett bejelentve. +\n +\n Ha nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + + Riotnak engedélyre van szüksége ahhoz, hogy a végponttól végpontig titkosító kulcsokat a lemezre menthesse. +\n +\nKérlek a következő felugró ablakban engedélyezd a hozzáférést, hogy a kulcsokat kézzel kimenthesd. + + Jelenleg nincs hálózati kapcsolat + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 0e497c8eb0..d46eaa987b 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1613,7 +1613,7 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi." +\nNon tutte le funzioni di Riot sono già implementate in RiotX. Principali funzioni mancanti (prossimamente!): • Creazione account • Impostazioni stanza (elenca membri stanza, ecc.) • Chiamate • Widget • … Non hai nulla di nuovo da vedere! Messaggi diretti @@ -1678,7 +1678,7 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Leggi su -Nessuno + Nessuno Revoca Disconnetti Nessun server di identità configurato. @@ -1743,6 +1743,75 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Il server di identità che hai scelto non ha alcuna condizione di servizio. Continua solo se ti fidi del proprietario del servizio È stato inviato un messaggio a %s. Inserisci il codice di verifica contenuto. - Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %s. Dovrai riconnetterti a %s per fermarne la condivisione. + Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %1$s. Dovrai riconnetterti a %2$s per fermarne la condivisione. Accetta le condizioni di servizio del server di identità (%s) per consentire di essere trovabile per email o numero di telefono. + Latn + + Attiva log dettagliati. + I log dettagliati aiuteranno gli sviluppatori fornendo loro più informazioni quando invii una segnalazione. Anche quando attivi, l\'applicazione non registra i contenuti dei messaggi o altri dati personali. + + + Riprova dopo avere accettato i termini e condizioni del tuo homeserver. + + Sembra che il server stia impiegando troppo tempo a rispondere, ciò può essere causato da scarsa connettività o un errore con i nostri server. Riprova fra qualche minuto. + + Invia allegato + + Apri il pannello di navigazione + Apri il menu di creazione stanza + Chiudi il menu di creazione stanza… + Crea una nuova conversazione diretta + Crea una nuova stanza + Chiudi il banner di backup chiavi + Mostra password + Nascondi password + Salta in fondo + + %1$s, %2$s ed altri %3$d hanno letto + %1$s, %2$s e %3$s hanno letto + %1$s e %2$s hanno letto + %s ha letto + + 1 utente ha letto + %d utenti hanno letto + + + Il file \'%1$s\' (%2$s) è troppo grande da inviare. Il limite è %3$s. + + Si è verificato un errore ricevendo l\'allegato. + File + Contatto + Fotocamera + Audio + Galleria + Adesivo + Errore di gestione dati condivisi + + È spam + È inappropriato + Segnalazione personalizzata + Segnala questo contenuto + Motivo della segnalazione + SEGNALA + BLOCCA UTENTE + + Contenuto segnalato + Questo contenuto è stato segnalato. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + Segnalato come spam + Questo contenuto è stato segnalato come spam. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + Segnalato come inappropriato + Questo contenuto è stato segnalato come inappropriato. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + + Riot richiede l\'autorizzazione per salvare le tue chiavi E2E su disco. +\n +\nPermetti l\'accesso nel prossimo pop-up per poter esportare le chiavi manualmente. + + Non c\'è nessuna connessione di rete al momento + diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 523bc83d14..3f2469d766 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -19,7 +19,7 @@ 불러오는 중… - + 취소 저장 떠나기 @@ -31,8 +31,8 @@ 다운로드 공유 고유 주소 - 출처 보기 - 해독된 출처 보기 + 소스 보기 + 해독된 소스 보기 삭제 다시 이름 짓기 내용 신고하기 @@ -235,7 +235,7 @@ 전화번호가 누락되었습니다 이메일 주소나 전화번호가 누락되었습니다 옳지 않은 토큰 - 맞춤 서버 옵션을 사용하기 (고급) + 맞춤 서버 설정을 사용하기 (고급) 등록을 계속하려면 이메일을 확인하세요 API가 존재하기 전까지는 이메일과 전화번호로 한 번에 등록할 수 없습니다. 오직 전화번호만 고려됩니다. \n @@ -302,8 +302,8 @@ 방 주제 전화 - 오는 전화에 Riot 기본 벨소리를 사용합니다 - 오는 전화 벨소리 + 수신 전화에 Riot 기본 벨소리를 사용합니다 + 수신 전화 벨소리 전화에 사용할 벨소리를 선택하세요: 전화 @@ -311,9 +311,9 @@ 전화 연결 중… 전화 종료됨 전화 중… - 오는 전화 - 오는 영상 통화 - 오는 음성 통화 + 수신 전화 + 수신 영상 통화 + 수신 음성 통화 전화 진행 중… 영상 통화 진행 중… @@ -336,7 +336,7 @@ 음성 통화를 하려면 Riot은 마이크에 접근하는 권한이 필요합니다. " \n -\n전화를 하려면 다은 팝업에서 접근을 허용해주세요." +\n전화를 하려면 다음 팝업에서 접근을 허용해주세요." 영상 통화를 하려면 Riot은 카메라와 마이크에 접근하는 권한이 필요합니다. \n \n전화를 하려면 다음 팝업에서 접근을 허용해주세요. @@ -412,8 +412,8 @@ 초대 이 방 떠나기 이 방에서 삭제하기 - 차단 - 차단 해제 + 출입 금지 + 출입 금지 풀기 추방 일반 사용자로 재 설정 중재자로 하기 @@ -432,7 +432,7 @@ 이 사용자들을 이 대화에서 추방하겠습니까\? - 이 사용자를 이 대화에서 차단하겠습니까\? + 이 사용자를 이 대화에서 출입 금지하겠습니까\? 이유 %s님을 이 대화에 초대하겠습니까\? @@ -545,7 +545,7 @@ 버전 버전 %s 이용 약관 - 제 3자 공지 + 제 3자 고지 저작권 개인 정보 정책 @@ -661,7 +661,7 @@ 알림 소리 이 계정에서 알림 켜기 이 기기에서 알림 켜기 - 3초 동안 화면을 켬 + 3초 동안 화면을 켜기 소리 알림 설정 전화 알림 설정 조용한 알림 설정 @@ -687,7 +687,7 @@ 버전 olm 버전 이용 약관 - 제 3자 공지 + 제 3자 고지 저작권 개인 정보 정책 미디어 유지 @@ -720,7 +720,7 @@ 읽은 기록 보이기 세부적인 목록으로 읽은 목록을 클릭하세요. 참가 및 떠남 이벤트 보이기 - 초대, 추방, 그리고 차단은 영향이 없습니다. + 초대, 추방, 그리고 출입 금지은 영향이 없습니다. 계정 이벤트 보이기 아바타와 표시 이름 변경도 포함합니다. 사용자가 언급할 때 진동 @@ -734,7 +734,7 @@ 알림 개인 정보 Riot은 백그라운드에서 실행되어 알림을 안전하고 은밀하게 관리할 수 있습니다. 이것은 배터리 사용량에 영향을 줄 수 있습니다. 권한 부여 - 다른 옵션을 선택하세요 + 다른 설정을 선택하세요 백그라운드 연결 Riot은 신뢰가 있는 알림을 위해 낮은 영향의 백그라운드 연결을 유지해야 합니다. @@ -845,7 +845,7 @@ 누가 이 방에 접근할 수 있나요\? 누구나 - (이 옵션을 선택한 시점부터) 구성원만 + (이 설정을 선택한 시점부터) 구성원만 (초대받은 시점부터) 구성원만 (참가한 시점부터) 구성원만 @@ -854,7 +854,7 @@ 방의 링크를 아는 누구나, 손님 제외 방의 링크를 아는 누구나, 손님 포함 - 차단한 사용자 + 출입 금지한 사용자 고급 이 방의 내부 ID @@ -931,7 +931,7 @@ 로컬 파일에서 키 가져오기 가져오기 확인된 기기로만 암호화 - 이 기기에서 확인되지 않은 기기로 절대 암호화된 메시지를 보내지 않기. + 이 기기에서 확인되지 않은 기기로 절대 암호화된 메시지를 보내지 않습니다. %1$d/%2$d 키를 성공적으로 가져왔습니다. 확인되지 않음 @@ -1036,7 +1036,7 @@ 맞춤 카메라 화면 대신 시스템 카메라를 실행합니다. 키보드 엔터 키로 메시지 보내기 음성 메시지 보내기 - 이 옵션은 메시지를 기록하기 위한 제 3자 애플리케이션이 필요합니다. + 이 설정은 메시지를 기록하기 위한 제 3자 애플리케이션이 필요합니다. 새 기기 \'%s\'을(를) 추가했습니다, 여기에는 암호화 키가 필요합니다. 새 기기에는 암호화 키가 필요합니다. @@ -1064,8 +1064,8 @@ 인식할 수 없는 명령어: %s \"%s\" 명령어는 더 많은 매개 변수가 필요하거나, 일부 매개 변수가 옳지 않습니다. 활동 표시하기 - 주어진 ID로 사용자 차단하기 - 주어진 ID로 사용자 차단 풀기 + 주어진 ID로 사용자 출입 금지하기 + 주어진 ID로 사용자 출입 금지 풀기 사용자의 권한 등급 정의하기 주어진 ID로 사용자 강등하기 주어진 ID 현재 방에 사용자 초대하기 @@ -1114,7 +1114,7 @@ 커뮤니티 관리자가 이 커뮤니티에 대한 자세한 설명을 제공하지 않았습니다. %2$s님에 의해 %1$s 방에서 추방당했습니다 - %2$s님에 의해 %1$s 방에서 차단당했습니다 + %2$s님에 의해 %1$s 방에서 출입 금지당했습니다 이유: %1$s 다시 참가하기 방 잊어버리기 @@ -1453,7 +1453,7 @@ 파일 \"%1$s\"에서 종단간 암호화 키 가져옴. Matrix SDK 버전 - 다른 제 3자 공지 + 다른 제 3자 고지 이 방을 이미 봤습니다! 빠른 리액션 @@ -1495,7 +1495,7 @@ \n \nRiotX 지원: • 존재하는 계정으로 로그인 • 방을 만들고 공공 방에 참가 • 초대를 수락하거나 거절 • 사용자 방 목록 • 방 세부 정보 보기 • 문자 메시지 보내기 • 첨부 파일 보내기 • 암호화된 방에서 메시지 읽고 쓰기 • 암호화: 종단간 암호화 키 백업, 고급 기기 확인, 키 공유 요청과 답장 • 푸시 알림 • 밝은 테마, 어두운 테마 그리고 검정 테마 \n -\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 다이렉트 대화 방 만들기 • 전화 • 위젯 • … +\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 전화 • 위젯 • … 다이렉트 메시지 @@ -1556,7 +1556,7 @@ 읽은 시간 -없음 + 없음 취소 연결 해제 설정된 ID 서버가 없습니다. @@ -1574,7 +1574,7 @@ 계정 복구 용 이메일을 설정합니다. 이메일이나 전화로 이후 알고 있는 사람들이 당신을 찾을 수 있는지 여부를 선택하는데 사용됩니다. 이 URL로는 홈서버에 접근할 수 없습니다, 확인해주세요 대체 전화 지원 서버 허용 - 홈서버가 전화를 지원하지 않는다면 %s을(를) 지원 서버롤 사용합니다 (전화하는 동안 IP 주소가 공유될 것입니다) + 홈서버가 전화를 지원하지 않는다면 %s을(를) 지원 서버로 사용합니다 (전화하는 동안 IP 주소가 공유됩니다) 이 작업을 하려면 설정에서 ID 서버를 추가하세요. 백그라운드 동기화 모드 (실험적) 배터리에 최적화됨 @@ -1584,7 +1584,7 @@ Riot은 (설정할 수 있는) 특정 시간에 주기적으로 백그라운드에거 동기화됩니다. \n이는 라디오와 배터리 사용에 영향을 주며 Riot이 이벤트를 수신하고 있는 상태라는 알림이 영구적으로 표시됩니다. 백그라운드 동기화 없음 - 앱이 백그라운드에 있을 때 오는 메시지의 알림을 받지 않습니다. + 앱이 백그라운드에 있을 때 수신 메시지의 알림을 받지 않습니다. 설정을 업데이트하는데 실패했습니다. @@ -1592,7 +1592,7 @@ %s \n동기화는 자원 (배터리)이나 기기의 상태 (수면)에 따라 지연됩니다. 탐색 - 탐색 설정 관리하기. + 탐색 설정을 관리합니다. 공개 이름 (대화하는 사람들에게 보여집니다) 기기의 공개 이름은 대화하는 사람들에게 보여집니다 ID 서버를 사용하고 있지 않습니다 @@ -1621,6 +1621,74 @@ 선택한 ID 서버가 서비스 약관이 없습니다. 서비스의 소유자를 신뢰하는 경우에만 계속하세요 %s(으)로 문자 메시지를 보냈습니다. 문자에 있는 확인 코드를 입력해주세요. - 현재 이메일 주소나 전화번호를 ID 서버 %s와 공유하고 있습니다. 공유하기를 중지하려면 %s(으)로 다시 연결해야 합니다. + 현재 이메일 주소나 전화번호를 ID 서버 %1$s와 공유하고 있습니다. 공유하기를 중지하려면 %2$s(으)로 다시 연결해야 합니다. ID 서버 (%s)의 서비스 약관에 동의하면 다른 사용자가 당신을 이메일 주소나 전화번호로 찾을 수 있게 됩니다. + Latn + + 상세 로그 켜기. + 상세 로그는 분노의 흔들기를 보낼 때 더 많은 로그를 제공해서 개발자에게 도움을 줍니다. 이 설정을 켜도 애플리케이션은 메시지 내용이나 다른 개인 정보를 기록하지 않습니다. + + + 홈서버의 이용 약관에 동의한 후 다시 시도해주세요. + + 서버의 응답 시간이 지연되고 있습니다. 연결 상태가 좋지 않거나 서버에서 오류가 발생했을지도 모릅니다. 나중에 다시 시도해주세요. + + 첨부 파일 보내기 + + 내비게이션 서랍 열기 + 방 만들기 메뉴 열기 + 방 만들기 메뉴 닫기… + 새 다이렉트 대화 만들기 + 새 방 만들기 + 키 백업 배너 닫기 + 비밀번호 보이기 + 비밀번호 감추기 + 맨 아래로 건너뛰기 + + %1$s님, %2$s님 외 %3$d명이 읽음 + %1$s님, %2$s님 그리고 %3$s님이 읽음 + %1$s님 그리고 %2$s님이 읽음 + %s님이 읽음 + + %d명이 읽음 + + + 파일 \'%1$s\' (%2$s)이(가) 업로드하기에 너무 큽니다. 제한은 %3$s입니다. + + 첨부 파일을 검색하는 중 오류가 발생했습니다. + 파일 + 연락처 + 카메라 + 소리 + 갤러리 + 스티커 + 공유 데이터를 처리할 수 없음 + + 스팸 문자입니다 + 부적절한 문자입니다 + 맞춤 신고 + 이 내용 신고하기 + 이 내용을 신고하는 이유 + 신고 + 사용자 출입 금지 + + 내용 신고됨 + 이 내용을 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + 스팸 문자로 신고됨 + 이 내용을 스팸 메일로 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + 부적절한 문자로 신고됨 + 이 내용을 부적절한 문자로 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + + Riot은 종단간 키를 디스크에 저장하려면 권한이 필요합니다. +\n +\n키를 수동으로 내보내려면 다음 팝업에서 접근을 허용해주세요. + + 현재 네트워크 연결이 없습니다 + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index f099593898..bacc675f86 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1510,7 +1510,7 @@ Gebruikers komen niet overeen Onbekende fout -Geen + Geen Intrekken Verbinding verbreken Nakijken @@ -1519,4 +1519,4 @@ Geen identiteitsserver geconfigureerd. Oproep mislukt door verkeerd geconfigureerde server - + diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 00ef36dc34..e815f01b9f 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -446,7 +446,7 @@ Синхронизация Включить фоновую синхронизацию Таймаут синхронизации - Задержка между запросами + Задержка между каждой синхронизацией секунда секунд @@ -474,10 +474,10 @@ Прикрепить комнаты с отключенными уведомлениями Прикрепить комнаты с непрочитанными сообщениями Устройства - Подробности о устройстве + Информация об устройстве ID - Имя - Имя устройства + Общеизвестное имя + Обновить публичное имя Последнее подключение %1$s @ %2$s Для этой операции требуется дополнительная проверка подлинности. @@ -611,9 +611,9 @@ Ошибка дешифровки Информация об устройстве отправителя - Имя устройства - Имя - ID устройства + Публичное имя + Публичное имя + ID Ключ устройства Проверка Ed25519 отпечаток @@ -1653,15 +1653,15 @@ RiotX - Matrix клиент следующего поколения Быстрый и легкий клиент для Matrix с новейшими фреймворками Android - RiotX - это новый клиент для матричного протокола (Matrix.org): открытой сети для безопасной децентрализованной связи. RiotX - это полная перезапись Riot Android клиента, основанная на полной перезаписи Matrix Android SDK. -\n -\nОговорка: Это бета-версия. В настоящее время RiotX находится в активной разработке и содержит ограничения и (надеемся, не слишком много) ошибки. Мы будем рады любым отзывам! -\n -\nПоддержка RiotX: - Войти в существующую учетную запись - Создать комнату и присоединиться к общей комнате - Принять и отклонить приглашения - Список комнат пользователей - Просмотр сведений о комнате - Отправить текстовые сообщения - Отправить вложение - Читать и писать сообщения в зашифрованных комнатах - Криптографически: Резервное копирование клавиш E2E, предварительная проверка устройства, запрос и ответ на общий доступ к ключам - Нажмите уведомление - Светлые, темные и черные темы -\n -\nНе все функции RiotX пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) особенности: - Создание учетной записи - Настройки комнат (список членов комнат и т.д.) - Создание прямых чатов - Вызовы - Виджеты - .… + RiotX - это новый клиент для матричного протокола (Matrix.org): открытой сети для безопасной децентрализованной связи. RiotX - это полная перезапись Riot Android клиента, основанная на полной перезаписи Matrix Android SDK. +\n +\nОговорка: Это бета-версия. В настоящее время RiotX находится в активной разработке и содержит ограничения и (надеемся, не слишком много) ошибки. Мы будем рады любым отзывам! +\n +\nПоддержка RiotX: - Войти в существующую учетную запись - Создать комнату и присоединиться к общедоступным комнатам - Принять и отклонить приглашения - Список комнат пользователей - Просмотр сведений о комнате - Отправить текстовые сообщения - Отправить вложение - Читать и писать сообщения в зашифрованных комнатах - Криптография: Резервное копирование клавиш E2E, предварительная проверка устройства, запрос и ответ на общий доступ к ключам - Нажмите уведомление - Светлые и черные темы +\n +\nНе все функции Riot пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) свойства: - Создание учетной записи - Настройки комнат (список членов комнат и т.д.) - Вызовы - Виджеты - .… - Предварительный просмотр открытой комнаты в RiotX пока не поддерживается. + Предварительный просмотр открытой комнаты в RiotX пока не поддерживается Прямые сообщения @@ -1695,7 +1695,7 @@ Ссылка скопирована в буфер обмена - Добавить по matrix ID + Добавить по Matrix ID Создание комнаты… "Результат не найден, используйте добавить matrix ID для поиска на сервере." Начните печатать, чтобы получить результат @@ -1715,7 +1715,7 @@ \nПодробнее об этом читайте здесь: \nhttps://medium.com/@RiotChat/36b4792ea0d6 - Правила пуш не определены + Пуш-правила не определены Нет зарегистрированных пуш-шлюзов Условия предоставления услуг @@ -1725,4 +1725,76 @@ Читать в + Никто + Отмена + Отключить + Сервер идентификации не настроен. + + Звонок не состоялся из-за неправильно настроенного сервера + Попросите администратора вашего домашнего сервера (%1$s) настроить TURN сервер, чтобы звонки работали надежно. +\n +\nКроме того, вы можете попробовать использовать публичный сервер по %2$s, но это будет не так надежно, и он предоставит ваш IP-адрес этому серверу. Вы также можете управлять этим в настройках. + Попробуйте использовать %s + Не спрашивай меня больше + + Установите адрес электронной почты для восстановления учетной записи, и позже она может будет найдена участниками, которые вас знают. + Установите телефон, и позже его могут опционально обнаруживать люди, которые вас знают. + Установите адрес электронной почты для восстановления аккаунта. Позже используйте электронную почту или телефон, чтобы их могли найти люди, которые вас знают. + Установите адрес электронной почты для восстановления аккаунта. Позже используйте электронную почту или телефон, чтобы их могли найти люди, которые вас знают. + Не удается связаться с домашним сервером по этому URL, пожалуйста, проверьте его + Разрешить резервный сервер помощи при вызове + Оптимизирован для батареи + Оптимизирован для работы в реальном времени + Без фоновой синхронизации + Не удалось обновить настройки. + + + Предпочтительный интервал синхронизации + Обнаружение + Будет использовать%s в качестве помощника, если ваш домашний сервер не предлагает его (ваш IP-адрес будет доступен во время разговора) + Добавьте идентификационный сервер в свои настройки, чтобы выполнить это действие. + Режим фоновой синхронизации (Экспериментальный) + Riot будет синхронизироваться в фоновом режиме таким образом, чтобы сохранить ограниченные ресурсы устройства (батарея). +\nВ зависимости от состояния ресурса вашего устройства, синхронизация может быть отложена операционной системой. + Riot будет синхронизироваться в фоновом режиме периодически в точное время (настраивается). +\nЭто повлияет на использование радио и батареи, появится постоянное уведомление о том, что Riot прислушивается к событиям. + Вы не будете уведомлены о входящих сообщениях, когда приложение находится в фоновом режиме. + %s +\nСинхронизация может быть отложена в зависимости от ресурсов (батареи) или состояния устройства (спящий режим). + Управляйте настройками обнаружения. + Публичное имя (видимое для людей, с которыми вы общаетесь) + Публичное имя устройства видны людям, с которыми вы общаетесь + Вы не используете какой-либо сервер идентификации + Идентификационный сервер не настроен, требуется сброс пароля. + + Похоже, вы пытаетесь подключиться к другому домашнему серверу. Вы хотите выйти\? + + Сервер идентификации + Отключить идентификационный сервер + Настроить идентификационный сервер + Изменить идентификационный сервер + В настоящее время вы используете %1$s для обнаружения и быть найденным вашими контактами. + "Вы в настоящее время не используете идентификационный сервер. Чтобы обнаружить и быть найденным вашими существующими контактами, настройте один из них ниже." + Видимые адреса электронной почты + Доступные номера телефонов + В ожидании + + Введите новый сервер идентификации + Не удалось подключиться к серверу идентификации + Пожалуйста, введите URL сервера идентификации + Сервер идентификации не имеет условий предоставления услуг + Параметры обнаружения появятся после добавления электронной почты. + Параметры поиска появятся после добавления номера телефона. + Отключение от сервера идентификации будет означать, что другие пользователи не смогут обнаружить вас, и вы не сможете приглашать других по электронной почте или по телефону. + Мы отправили вам электронное письмо с подтверждением на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения + Выбранный сервер идентификации не имеет условий обслуживания. Продолжить, только если вы доверяете владельцу службы + Текстовое сообщение отправлено %s. Введите код проверки, который он содержит. + + В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими. + Примите Условия обслуживания сервера идентификации (%s), чтобы разрешить обнаружение по адресу электронной почты или номеру телефона. + Включить подробное журнал. + Отправить вложенное + + Откройте навигационный ящик + Откройте меню «Создать комнату» diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 8e439a41fe..f299ad4385 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -408,7 +408,7 @@ Njëkohësim në prapaskenë Aktivizo njëkohësim në prapaskenë Mbarim kohe për kërkesë njëkohësimi - Vonesë mes çdo kërkese + Vonesë mes çdo Njëkohësimi sekondë sekonda @@ -459,10 +459,10 @@ Mënyrë ruajtjeje të dhënash - Hollësi pajisjeje + Të dhëna pajisjeje ID - Emër - Emër Pajisjeje + Emër Publik + Përditësoni Emër Publik Parë së fundi më %1$s @ %2$s Mirëfilltësim @@ -572,9 +572,9 @@ Gabim shfshehtëzimi Të dhëna pajisjeje dërguesi - Emër pajisjeje - Emër - ID Pajisjeje + Emër publik + Emër publik + ID Kyç pajisjeje Verifikim Shenja gishtash Ed25519 @@ -1521,7 +1521,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani \n \nRiotX-i mbulon: • Hyrje në një llogari ekzistuese • Krijim dhome dhe pjesëmarrje në dhoma publike • Pranim dhe hedhje poshtë ftesash • Njohje të dhomave të përdoruesve • Parje hollësish dhome • Dërgim mesazhesh tekst • Dërgim bashkëngjitjesh • Lexim dhe shkrim mesazhesh në dhoma të fshehtëzuara • Kriptografi: kopjeruajtje kyçesh E2E, verifikim i thelluar pajisjesh, kërkesa dhe përgjigje për ndarje kyçesh • Njoftime push • Tema të Çelëta, të Errëta dhe të Zeza \n -\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Krijim llogarish • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Krijim dhomash fjalosjeje të drejtpërdrejtë • Thirrje • Widget-es • … +\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Krijim llogarish • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Thirrje • Widget-es • … Përgjegjës Integrimesh @@ -1587,4 +1587,71 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Lexoni te + Asnjë + Shfuqizoje + Shkëputu + S’ka shërbyes identitetesh të formësuar. + + Thirrja dështoi për shkak shërbyesi të keqformësuar + Që thirrjet të funksionojnë në mënyrë të qëndrueshme, ju lutemi, kërkojini përgjegjësit të shërbyesit tuaj Home (%1$s) të formësojë një shërbyes TURN. +\n +\nNdryshe, mund të provoni të përdorni shërbyesin publik te %2$s, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do të jetë e ditur për atë shërbyes. Këtë mund ta administroni edhe që nga Rregullimet. + Provoni të përdorni %s + Mos më pyet sërish + + Caktoni një email për rimarrje llogarie, dhe më vonë të jetë i zbulueshëm (opsionale) nga persona që ju njohin. + Caktoni një telefon, dhe më vonë të jetë i zbulueshëm (opsionale) nga persona që ju njohin. + Caktoni një email për rimarrje llogarie. Përdoni email ose telefon që të jeni i zbulueshëm (opsionale) nga persona që ju njohin. + Caktoni një email për rimarrje llogarie. Përdorni email ose telefon që të jeni i zbulueshëm (opsionale) nga persona që ju njohin. + S’kapet dot shërbyes Home te kjo URL, ju lutemi, kontrollojeni + Do të përdoret %s si ndihmë kur shërbyesi juaj Home nuk ofron të tillë (gjatë thirrjes, adresa juaj IP do të ndahet me të tjerë) + Që të kryhet ky veprim, shtoni një shërbyes identitetesh, që nga rregullimet tuaja. + Mënyrë Njëkohësimi Në Prapaskenë (Eksperimentale) + E optimizuar për baterinë + Riot-i do të bëjë njëkohësim në prapaskenë, në një mënyrë që kursen burimet e kufizuara të pajisjes (baterinë). +\nNë varësi të gjendjes së burimeve tuaja, njëkohësimi mund të shtyhet për më vonë nga sistemi operativ. + I optimizuar për kohë të njëmendtë + Riot0-i do të bëjë njëkohësim në prapaskenë periodikisht në një kohë të caktuar (e formësueshme). +\nKjo do të ketë ndikim mbi përdorimin e baterisë dhe të transmetimit, do të shfaqet një njoftim i pandërprerë që pohon se Riot-i po përgjon për akte. + Pa njëkohësim në prapraskenë + S’do të njoftoheni për mesazhe ardhës, kur aplikacioni gjendet në prapaskenë. + S’u arrit të përditësohen rregullime. + + + Interval i Parapëlqyer Njëkohësimesh + %s +\nNjëkohësimi mund të shtyhet për më vonë, në varësi të burimeve (baterisë) ose gjendjes së pajisjes (dremitje). + Administroni rregullimet tuaja për zbulime. + Emër publik (i dukshëm për persona me të cilët komunikoni) + Emri publik i një pajisjeje është i dukshëm për persona me të cilët komunikoni + S’po përdorni ndonjë Shërbyes Identitetesh + S’ka shërbyes identitetesh të formësuar, kjo është e domosdoshme për ricaktimin e fjalëkalimit tuaj. + + Duket se po rrekeni të lidheni me një tjetër shërbyes Home. Doni të bëhet dalja\? + + Që të përgjigjeni te rrjedha kohore, aktivizoni fërkimin + + Shërbyes identitetesh + Shkëpute shërbyesin e identiteteve + Formësoni shërbyes identitetesh + Ndryshoni shërbyes identitetesh + Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni. + S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jini i zbulueshëm nga kontakte ekzistuese që njihni, formësoni një të tillë më poshtë. + Adresa email të zbulueshme + Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email. + Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni. + Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm prej përdoruesish të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë me email ose telefon. + Numra telefoni të zbulueshëm + Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit + Pezull + + Jepni një shërbyes të ri identitetesh + S’u lidh dot te shërbyes identitetesh + Ju lutemi, jepni URL-në e shërbyesit të identiteteve + Shërbyesi i identiteteve s’ka kushte shërbimi + Shërbyesi i identiteteve që keni zgjedhur nuk ka ndonjë kusht shërbimi. Vazhdoni vetëm nëse i zini besë të zotit të shërbimit + Te %s u dërgua një mesazh tekst. Ju lutemi, jepni kodin e verifikimit që përmban ai. + + Hëpërhë, ndani me të tjerë adresa email ose numra telefoni te shërbyesi i identiteteve %1$s. Do të duhet të rilidheni me %2$s që të ndalni ndarjen e tyre. + Që të lejoni veten të jetë e zbulueshme nga adresë email apo numër telefoni, pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%s). diff --git a/vector/src/main/res/values-sr/strings.xml b/vector/src/main/res/values-sr/strings.xml index a6b3daec93..121589a6cd 100644 --- a/vector/src/main/res/values-sr/strings.xml +++ b/vector/src/main/res/values-sr/strings.xml @@ -1,2 +1,113 @@ - - \ No newline at end of file + + + sr + RS + Cyrl + + Светла тема + Тамна тема + Црна тема + Status.im тема + + Иницијализација сервиса + Синхронизација у току… + Бучна обавештења + Тиха обавештења + + Поруке + Соба + Подешавања + Подаци о члану + Историјски + Пријава грешке + Пошаљи налепницу + Резервна копија кључева + Користи резервну копију кључева + Верификуј уређај + + Креирање резервне копије кључева се није завршило, молим сачекајте… + Изгубићете ваше шифроване поруке ако се сад одјавите + Креирање резервне копије кључева је у току. Ако се одјавите сад, изгубићете приступ вашим шифрованим порукама. + Сигурносна копија кључева би требало да буде активна на свим вашим уређајима како би избегли губитак приступа вашим шифрованим порукама. + Не желим моје шифроване поруке + Прављење резервне копије кључева у току… + Користи резервну копију кључева + Да ли сте сигурни\? + Изгубићете приступ вашим шифрованим порукама уколико не направите резервну копију кључева пре него што се одјавите. + + Учитавање… + + У реду + Откажи + Сачувај + Напусти + Остани + Пошаљи + Копирај + Пошаљи поново + Уклони + Подели + Прихвати + Прескочи + Готово + Обустави + Игнориши + Прегледај + Одбаци + + Изађи + Акције + Одјави се + Да ли сте сигурни да желите да се одјавите\? + Гласовни позив + Видео позив + Глобална претрага + Означи све као прочитано + Брзи одговор + Означи као прочитано + Отвори + Затвори + Онемогући + + Потврда + Упозорење + Грешка + + Омиљено + Људи + Собе + Позивнице + Низак приоритет + Разговори + Локални адресар + Листа корисника + Само Matrix контакти + Нема резултата + Нема подешених сервера идентитета. + + Собе + Листа соба + Нема соба + Пошаљи позивницу + Опишите ваш проблем овде + Прочитај + + Придружи се соби + Корисничко име + Направи налог + Пријави се + Одјави се + Пошаљи налепницу + Направи фотографију или видео снимак + Направи фотографију + Направи видео снимак + + Пријави се + Пријави се помоћу single sign-on + Направи налог + Прескочи + Адреса електронске поште или корисничко име + Лозинка + Нова лозинка + Корисничко име + diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 9af87fd740..a1d6ceb32b 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1522,7 +1522,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 \n \nRiotX 支援:• 登入到既有的帳號 • 建立聊天室與加入公開聊天室 • 接受與回絕邀請 • 列出使用者聊天室 • 檢視聊天室詳細資訊 • 傳送文字訊息 • 傳送附件 • 讀取與編寫已加密的聊天室 • 加密:E2E 金鑰備份、進階裝置驗證、金鑰分享請求與回應 • 推送通知 • 亮、暗與黑色主題 \n -\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能:• 建立帳號 • 聊天室設定(列出聊天室成員等) • 直接聊天室建立 • 通話 • 小工具 • … +\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能:• 建立帳號 • 聊天室設定(列出聊天室成員等) • 通話 • 小工具 • … 直接訊息 @@ -1586,7 +1586,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 閱讀於 - + 撤銷 斷線 未設定身份識別伺服器。 @@ -1651,6 +1651,73 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 您選擇的身份識別伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續 文字訊息已傳送給 %s。請輸入其中包含的驗證碼。 - 您目前正在身份識別伺服器 %s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %s 以停止分享它們。 + 您目前正在身份識別伺服器 %1$s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %2$s 以停止分享它們。 同意身份識別伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。 + + 啟用詳細紀錄。 + 詳細紀錄可以協助開發者在您傳送憤怒搖晃時取得更多紀錄。即使啟用這個設定,應用程式依然不會紀錄訊息內容或任何個人資料。 + + + 請在您接受您家伺服器的條款與條件前繼續重試。 + + 看起來伺服器回應時間似乎太久了,這可能是不良的網路連線或我們的伺服器錯誤所造成。請稍後再試。 + + 傳送附件 + + 開啟導航選單 + 開啟建立聊天室選單 + 關閉建立聊天室選單…… + 建立新的直接對話 + 建立新的聊天室 + 關閉金鑰備份橫幅 + 顯示密碼 + 隱藏密碼 + 跳到底部 + + %1$s、%2$s 與 %3$d 個其他人已閱讀 + %1$s、%2$s 與 %3$s 已閱讀 + %1$s 與 %2$s 已閱讀 + %s 已閱讀 + + %d 個使用者已閱讀 + + + 檔案「%1$s」(%2$s) 太大無法上傳。限制為 %3$s。 + + 在擷取附件時遇到錯誤。 + 檔案 + 聯絡人 + 相機 + 音訊 + 相簿 + 貼圖 + 無法處理分享資料 + + 垃圾訊息 + 不合適 + 自訂回報 + 回報此內容 + 回報此內容的理由 + 回報 + 封鎖使用者 + + 內容已回報 + 此內容已回報。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + 回報為垃圾訊息 + 此內容已被回報為垃圾訊息。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + 回報為不合適 + 此內容已被回報為不合適。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + + Riot 需要權限以在磁碟上儲存您的 E2E 金鑰。 +\n +\n請在下個彈出視窗中允許存取以讓您可以手動匯出您的金鑰。 + + 目前沒有網路連線 + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index de8d1d9e22..f0e7d88a07 100755 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -5,6 +5,8 @@ en US + + Latn Light Theme @@ -628,6 +630,9 @@ Add phone number Application info Show the application info in the system settings. + Confirm your password + You can\'t do this from Riot mobile + Authentication is required Advanced Notification Settings @@ -768,6 +773,8 @@ Ignored users Other Advanced + Integrations + Use an Integration Manager to manage bots, bridges, widgets and sticker packs.\nIntegration Managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf. Cryptography Cryptography Keys Management Notification Targets @@ -834,6 +841,7 @@ Logged in as Home Server Identity Server + Allow integrations Integration Manager User interface @@ -1106,7 +1114,32 @@ %d active widgets + + Widget + Load Widget + This widget was added by: + Using it may set cookies and share data with %s: + Using it may share data with %s: + Failed to load widget.\n%s + Reload widget + Open in browser + Revoke access for me + + Your display name + Your avatar URL + Your user ID + Your theme + Widget ID + Room ID + + Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 5.0) + This widget wants to use the following resources: + Allow + Block All + Use the camera + Use the microphone + Read DRM protected Media Unable to create widget. @@ -1626,7 +1659,6 @@ RiotX supports: Not all features in Riot are implemented in RiotX yet. Main missing (and coming soon!) features: • Account creation • Room settings (list room members, etc.) -• Creation of direct chat rooms • Calls • Widgets • …" @@ -1705,6 +1737,67 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming The identity server you have chosen does not have any terms of services. Only continue if you trust the owner of the service A text message has been sent to %s. Please enter the verification code it contains. - You are currently sharing email addresses or phone numbers on the identity server %s. You will need to reconnect to %s to stop sharing them. + You are currently sharing email addresses or phone numbers on the identity server %1$s. You will need to reconnect to %2$s to stop sharing them. Agree to the identity server (%s) Terms of Service to allow yourself to be discoverable by email address or phone number. + + Enable verbose logs. + Verbose logs will help developers by providing more logs when you send a RageShake. Even when enabled, the application does not log message contents or any other private data. + + + Please retry once you have accepted the terms and conditions of your homeserver. + + Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while. + + Send attachment + + Open the navigation drawer + Open the create room menu + Close the create room menu… + Create a new direct conversation + Create a new room + Close keys backup banner + Show password + Hide password + Jump to bottom + + + %1$s, %2$s and %3$d others read + %1$s, %2$s and %3$s read + %1$s and %2$s read + %s read + + 1 user read + %d users read + + + "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." + + "An error occurred while retrieving the attachment." + "File" + "Contact" + "Camera" + "Audio" + "Gallery" + "Sticker" + Couldn\'t handle share data + + "It's spam" + "It's inappropriate" + "Custom report" + "Report this content" + "Reason for reporting this content" + "REPORT" + "BLOCK USER" + + "Content reported" + "This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages" + "Reported as spam" + "This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages" + "Reported as inappropriate" + "This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages" + + Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually. + + There is no network connection right now + diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 9465ab6254..33b9471e6c 100755 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -48,7 +48,7 @@ android:persistent="false" android:title="@string/settings_discovery_category" android:summary="@string/settings_discovery_manage" - app:fragment="im.vector.fragments.discovery.VectorSettingsDiscoveryFragment" /> + app:fragment="im.vector.fragments.discovery.VectorSettingsDiscoveryFragment" /> @@ -444,14 +444,30 @@ android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" android:title="@string/settings_identity_server" tools:summary="@string/default_identity_server_url" - app:fragment="im.vector.fragments.discovery.VectorSettingsDiscoveryFragment"/> + app:fragment="im.vector.fragments.discovery.VectorSettingsDiscoveryFragment" /> + + + + + + + + + -