From 4f0923ba0df755062ab59dd155441339be4ec3f0 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Mon, 22 Mar 2021 16:57:30 +0100 Subject: [PATCH 01/16] Add user profile, allow to edit it, if server supports it Signed-off-by: tobiasKaminsky --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 1 + .../java/com/nextcloud/talk/api/NcApi.java | 25 +- .../adapters/items/BrowserFileItem.java | 21 +- .../controllers/BrowserController.java | 90 +- .../BrowserForAvatarController.java | 59 ++ .../BrowserForSharingController.java | 80 ++ .../talk/controllers/ChatController.kt | 3 +- .../talk/controllers/ProfileController.java | 800 ++++++++++++++++++ .../talk/controllers/SettingsController.java | 44 +- .../talk/dagger/modules/ContextModule.java | 1 + .../talk/interfaces/SelectionInterface.kt | 2 + .../nextcloud/talk/models/database/User.java | 33 + .../json/capabilities/Capabilities.java | 7 +- .../capabilities/ProvisioningCapability.java | 36 + .../json/converters/ScopeConverter.java | 47 + .../talk/models/json/userprofile/Scope.java | 38 + .../json/userprofile/UserProfileData.java | 74 +- .../userprofile/UserProfileFieldsOCS.java | 39 + .../userprofile/UserProfileFieldsOverall.java | 36 + .../nextcloud/talk/ui/dialog/ScopeDialog.kt | 70 ++ .../com/nextcloud/talk/utils/ApiUtils.java | 13 +- .../nextcloud/talk/utils/DisplayUtils.java | 80 ++ app/src/main/res/drawable/ic_contacts.xml | 9 + app/src/main/res/drawable/ic_email.xml | 23 + app/src/main/res/drawable/ic_link.xml | 9 + .../main/res/drawable/ic_list_empty_error.xml | 5 + app/src/main/res/drawable/ic_map_marker.xml | 23 + app/src/main/res/drawable/ic_password.xml | 9 + app/src/main/res/drawable/ic_phone.xml | 23 + app/src/main/res/drawable/ic_twitter.xml | 23 + app/src/main/res/drawable/ic_user.xml | 25 + app/src/main/res/drawable/ic_web.xml | 23 + app/src/main/res/drawable/round_corner.xml | 5 + app/src/main/res/drawable/trashbin.xml | 25 + app/src/main/res/drawable/upload.xml | 12 + .../main/res/layout/controller_profile.xml | 219 +++++ .../main/res/layout/controller_settings.xml | 1 + app/src/main/res/layout/dialog_scope.xml | 184 ++++ app/src/main/res/layout/empty_list.xml | 64 ++ .../layout/user_info_details_table_item.xml | 70 ++ app/src/main/res/menu/menu_profile.xml | 28 + app/src/main/res/values/dimens.xml | 12 + app/src/main/res/values/strings.xml | 31 +- 44 files changed, 2334 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java create mode 100644 app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.java create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt create mode 100644 app/src/main/res/drawable/ic_contacts.xml create mode 100644 app/src/main/res/drawable/ic_email.xml create mode 100644 app/src/main/res/drawable/ic_link.xml create mode 100644 app/src/main/res/drawable/ic_list_empty_error.xml create mode 100644 app/src/main/res/drawable/ic_map_marker.xml create mode 100644 app/src/main/res/drawable/ic_password.xml create mode 100644 app/src/main/res/drawable/ic_phone.xml create mode 100644 app/src/main/res/drawable/ic_twitter.xml create mode 100644 app/src/main/res/drawable/ic_user.xml create mode 100644 app/src/main/res/drawable/ic_web.xml create mode 100644 app/src/main/res/drawable/round_corner.xml create mode 100644 app/src/main/res/drawable/trashbin.xml create mode 100644 app/src/main/res/drawable/upload.xml create mode 100644 app/src/main/res/layout/controller_profile.xml create mode 100644 app/src/main/res/layout/dialog_scope.xml create mode 100644 app/src/main/res/layout/empty_list.xml create mode 100644 app/src/main/res/layout/user_info_details_table_item.xml create mode 100644 app/src/main/res/menu/menu_profile.xml diff --git a/app/build.gradle b/app/build.gradle index 786d1b581..b7f9e990e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,6 +62,7 @@ android { lintOptions { disable 'InvalidPackage' disable 'MissingTranslation' + disable 'VectorPath' } javaCompileOptions { @@ -240,6 +241,10 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' + //implementation 'com.github.dhaval2404:imagepicker:1.8' + implementation 'com.github.tobiaskaminsky:ImagePicker:extraFile-SNAPSHOT' + implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0' + testImplementation 'junit:junit:4.13' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.powermock:powermock-core:2.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 494b5ab36..d10568e05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" android:theme="@style/AppTheme" + android:requestLegacyExternalStorage="true" tools:ignore="UnusedAttribute" tools:replace="label, icon, theme, name, allowBackup"> diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 9ee3c6661..821a02a6d 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -20,8 +20,6 @@ */ package com.nextcloud.talk.api; -import androidx.annotation.Nullable; - import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; import com.nextcloud.talk.models.json.chat.ChatOverall; import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage; @@ -37,14 +35,18 @@ import com.nextcloud.talk.models.json.push.PushRegistrationOverall; import com.nextcloud.talk.models.json.search.ContactsByNumberOverall; import com.nextcloud.talk.models.json.signaling.SignalingOverall; import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; +import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; import java.util.List; import java.util.Map; +import androidx.annotation.Nullable; import io.reactivex.Observable; +import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; +import retrofit2.Call; import retrofit2.Response; import retrofit2.http.Body; import retrofit2.http.DELETE; @@ -53,8 +55,10 @@ import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.PUT; +import retrofit2.http.Part; import retrofit2.http.Query; import retrofit2.http.QueryMap; import retrofit2.http.Url; @@ -373,4 +377,21 @@ public interface NcApi { @DELETE Observable deleteChatMessage(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable deleteAvatar(@Header("Authorization") String authorization, @Url String url); + + @Multipart + @POST + Observable uploadAvatar(@Header("Authorization") String authorization, + @Url String url, + @Part MultipartBody.Part attachment); + + @GET + Observable getEditableUserProfileFields(@Header("Authorization") String authorization, + @Url String url); + + @GET + Call downloadResizedImage(@Header("Authorization") String authorization, + @Url String url); } diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java index f57917644..1ec98bd66 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.java @@ -43,15 +43,20 @@ import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.DateUtils; import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DrawableUtils; + +import java.util.List; + +import javax.inject.Inject; + +import autodagger.AutoInjector; +import butterknife.BindView; +import butterknife.ButterKnife; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flexibleadapter.items.IFilterable; import eu.davidea.flexibleadapter.items.IFlexible; import eu.davidea.viewholders.FlexibleViewHolder; -import javax.inject.Inject; -import java.util.List; - @AutoInjector(NextcloudTalkApplication.class) public class BrowserFileItem extends AbstractFlexibleItem implements IFilterable { @Inject @@ -125,6 +130,16 @@ public class BrowserFileItem extends AbstractFlexibleItem selectedPaths; + protected final Set selectedPaths; @Inject UserUtils userUtils; @BindView(R.id.recycler_view) @@ -88,8 +98,7 @@ public class BrowserController extends BaseController implements ListingInterfac private ListingAbstractClass listingAbstractClass; private BrowserType browserType; private String currentPath; - private UserEntity activeUser; - private String roomToken; + protected UserEntity activeUser; public BrowserController(Bundle args) { super(args); @@ -97,7 +106,6 @@ public class BrowserController extends BaseController implements ListingInterfac NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); browserType = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_BROWSER_TYPE())); activeUser = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY())); - roomToken = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()); currentPath = "/"; if (BrowserType.DAV_BROWSER.equals(browserType)) { @@ -109,6 +117,7 @@ public class BrowserController extends BaseController implements ListingInterfac selectedPaths = Collections.synchronizedSet(new TreeSet<>()); } + @NotNull @Override protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { return inflater.inflate(R.layout.controller_browser, container, false); @@ -125,38 +134,10 @@ public class BrowserController extends BaseController implements ListingInterfac prepareViews(); } - private void onFileSelectionDone() { - synchronized (selectedPaths) { - Iterator iterator = selectedPaths.iterator(); - - List paths = new ArrayList<>(); - Data data; - OneTimeWorkRequest shareWorker; - - while (iterator.hasNext()) { - String path = iterator.next(); - paths.add(path); - iterator.remove(); - if (paths.size() == 10 || !iterator.hasNext()) { - data = new Data.Builder() - .putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), activeUser.getId()) - .putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomToken) - .putStringArray(BundleKeys.INSTANCE.getKEY_FILE_PATHS(), paths.toArray(new String[0])) - .build(); - shareWorker = new OneTimeWorkRequest.Builder(ShareOperationWorker.class) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueue(shareWorker); - paths = new ArrayList<>(); - } - } - } - - getRouter().popCurrentController(); - } + abstract void onFileSelectionDone(); @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_share_files, menu); filesSelectionDoneMenuItem = menu.findItem(R.id.files_selection_done); @@ -315,7 +296,7 @@ public class BrowserController extends BaseController implements ListingInterfac @SuppressLint("RestrictedApi") @Override - public void toggleBrowserItemSelection(String path) { + public void toggleBrowserItemSelection(@NonNull String path) { if (selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)) { checkAndRemoveAnySelectedParents(path); } else { @@ -327,10 +308,13 @@ public class BrowserController extends BaseController implements ListingInterfac } @Override - public boolean isPathSelected(String path) { + public boolean isPathSelected(@NonNull String path) { return (selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)); } + @Override + abstract public boolean shouldOnlySelectOneImageFile(); + @Parcel public enum BrowserType { FILE_BROWSER, diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java new file mode 100644 index 000000000..6ca11a6da --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForAvatarController.java @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.components.filebrowser.controllers; + +import android.content.Intent; +import android.os.Bundle; + +import com.nextcloud.talk.controllers.ProfileController; + +import androidx.annotation.Nullable; + +public class BrowserForAvatarController extends BrowserController { + private ProfileController controller; + + public BrowserForAvatarController(Bundle args) { + super(args); + } + + public BrowserForAvatarController(Bundle args, ProfileController controller) { + super(args); + + this.controller = controller; + } + + @Override + void onFileSelectionDone() { + controller.handleAvatar(selectedPaths.iterator().next()); + + getRouter().popCurrentController(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean shouldOnlySelectOneImageFile() { + return true; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java new file mode 100644 index 000000000..1c045af4b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserForSharingController.java @@ -0,0 +1,80 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.components.filebrowser.controllers; + +import android.os.Bundle; + +import com.nextcloud.talk.jobs.ShareOperationWorker; +import com.nextcloud.talk.utils.bundle.BundleKeys; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +public class BrowserForSharingController extends BrowserController { + private final String roomToken; + + public BrowserForSharingController(Bundle args) { + super(args); + + roomToken = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()); + } + + @Override + void onFileSelectionDone() { + synchronized (selectedPaths) { + Iterator iterator = selectedPaths.iterator(); + + List paths = new ArrayList<>(); + Data data; + OneTimeWorkRequest shareWorker; + + while (iterator.hasNext()) { + String path = iterator.next(); + paths.add(path); + iterator.remove(); + if (paths.size() == 10 || !iterator.hasNext()) { + data = new Data.Builder() + .putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), activeUser.getId()) + .putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomToken) + .putStringArray(BundleKeys.INSTANCE.getKEY_FILE_PATHS(), paths.toArray(new String[0])) + .build(); + shareWorker = new OneTimeWorkRequest.Builder(ShareOperationWorker.class) + .setInputData(data) + .build(); + WorkManager.getInstance().enqueue(shareWorker); + paths = new ArrayList<>(); + } + } + } + + getRouter().popCurrentController(); + } + + @Override + public boolean shouldOnlySelectOneImageFile() { + return false; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index f960d8688..f66e1014b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -72,6 +72,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback import com.nextcloud.talk.components.filebrowser.controllers.BrowserController +import com.nextcloud.talk.components.filebrowser.controllers.BrowserForSharingController import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent @@ -667,7 +668,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType)) bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - router.pushController(RouterTransaction.with(BrowserController(bundle)) + router.pushController(RouterTransaction.with(BrowserForSharingController(bundle)) .pushChangeHandler(VerticalChangeHandler()) .popChangeHandler(VerticalChangeHandler())) } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java new file mode 100644 index 000000000..3fa3733c2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -0,0 +1,800 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.controllers; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; +import com.github.dhaval2404.imagepicker.ImagePicker; +import com.nextcloud.talk.R; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.components.filebrowser.controllers.BrowserController; +import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController; +import com.nextcloud.talk.controllers.base.BaseController; +import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.models.json.generic.GenericOverall; +import com.nextcloud.talk.models.json.userprofile.Scope; +import com.nextcloud.talk.models.json.userprofile.UserProfileData; +import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; +import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; +import com.nextcloud.talk.ui.dialog.ScopeDialog; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.DisplayUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; +import com.nextcloud.talk.utils.database.user.UserUtils; + +import org.jetbrains.annotations.NotNull; +import org.parceler.Parcels; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import javax.inject.Inject; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; +import autodagger.AutoInjector; +import butterknife.BindView; +import butterknife.ButterKnife; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@AutoInjector(NextcloudTalkApplication.class) +public class ProfileController extends BaseController { + @Inject + NcApi ncApi; + + @Inject + UserUtils userUtils; + private UserEntity currentUser; + private boolean edit = false; + private RecyclerView recyclerView; + private UserProfileData userInfo; + private ArrayList editableFields = new ArrayList<>(); + + public ProfileController() { + super(); + } + + @NotNull + protected View inflateView(@NotNull LayoutInflater inflater, @NotNull ViewGroup container) { + return inflater.inflate(R.layout.controller_profile, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + inflater.inflate(R.menu.menu_profile, menu); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + + menu.findItem(R.id.edit).setVisible(editableFields.size() > 0); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.edit: + if (edit) { + save(); + } + + edit = !edit; + + if (edit) { + item.setTitle(R.string.save); + + if (currentUser.isAvatarEndpointAvailable()) { + // TODO later avatar can also be checked via user fields, for now it is in Talk capability + getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.VISIBLE); + } + + ncApi.getEditableUserProfileFields(ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ApiUtils.getUrlForUserFields(currentUser.getBaseUrl())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull UserProfileFieldsOverall userProfileFieldsOverall) { + editableFields = userProfileFieldsOverall.getOcs().getData(); + recyclerView.getAdapter().notifyDataSetChanged(); + } + + @Override + public void onError(@NotNull Throwable e) { + edit = false; + } + + @Override + public void onComplete() { + + } + }); + } else { + item.setTitle(R.string.edit); + getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE); + } + + recyclerView.getAdapter().notifyDataSetChanged(); + + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onAttach(@NonNull View view) { + super.onAttach(view); + + recyclerView = getActivity().findViewById(R.id.userinfo_list); + recyclerView.setAdapter(new UserInfoAdapter(null, + getActivity().getResources().getColor(R.color.colorPrimary), + this)); + recyclerView.setItemViewCacheSize(20); + + currentUser = userUtils.getCurrentUser(); + String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); + + getActivity().findViewById(R.id.avatar_upload).setOnClickListener(v -> sendSelectLocalFileIntent()); + getActivity().findViewById(R.id.avatar_choose).setOnClickListener(v -> + showBrowserScreen(BrowserController.BrowserType.DAV_BROWSER)); + + getActivity().findViewById(R.id.avatar_delete).setOnClickListener(v -> + ncApi.deleteAvatar(credentials, ApiUtils.getUrlForTempAvatar(currentUser.getBaseUrl())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull GenericOverall genericOverall) { + DisplayUtils.loadAvatarImage(currentUser, + getActivity().findViewById(R.id.avatar_image)); + } + + @Override + public void onError(@NotNull Throwable e) { + Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_LONG).show(); + } + + @Override + public void onComplete() { + + } + })); + + ViewCompat.setTransitionName(getActivity().findViewById(R.id.avatar_image), "userAvatar.transitionTag"); + + ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser.getBaseUrl())) + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull UserProfileOverall userProfileOverall) { + userInfo = userProfileOverall.getOcs().getData(); + showUserProfile(); + } + + @Override + public void onError(@NotNull Throwable e) { + setErrorMessageForMultiList( + getActivity().getString(R.string.userinfo_no_info_headline), + getActivity().getString(R.string.userinfo_error_text), + R.drawable.ic_list_empty_error); + } + + @Override + public void onComplete() { + + } + }); + } + + private void showUserProfile() { + if (getActivity() == null) { + return; + } + ((TextView) getActivity() + .findViewById(R.id.userinfo_baseurl)) + .setText(Uri.parse(currentUser.getBaseUrl()).getHost()); + + DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image)); + + if (!TextUtils.isEmpty(userInfo.getDisplayName())) { + ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(userInfo.getDisplayName()); + } + + if (TextUtils.isEmpty(userInfo.getPhone()) && + TextUtils.isEmpty(userInfo.getEmail()) && + TextUtils.isEmpty(userInfo.getAddress()) && + TextUtils.isEmpty(userInfo.getTwitter()) && + TextUtils.isEmpty(userInfo.getWebsite())) { + + getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE); + getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE); + getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE); + + setErrorMessageForMultiList( + getActivity().getString(R.string.userinfo_no_info_headline), + getActivity().getString(R.string.userinfo_no_info_text), R.drawable.ic_user); + } else { + getActivity().findViewById(R.id.loading_content).setVisibility(View.VISIBLE); + getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE); + + RecyclerView recyclerView = getActivity().findViewById(R.id.userinfo_list); + if (recyclerView.getAdapter() instanceof UserInfoAdapter) { + UserInfoAdapter adapter = ((UserInfoAdapter) recyclerView.getAdapter()); + adapter.setData(createUserInfoDetails(userInfo)); + } + + getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE); + getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE); + } + + // show edit button + if (currentUser.canEditScopes()) { + ncApi.getEditableUserProfileFields(ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ApiUtils.getUrlForUserFields(currentUser.getBaseUrl())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull UserProfileFieldsOverall userProfileFieldsOverall) { + editableFields = userProfileFieldsOverall.getOcs().getData(); + + getActivity().invalidateOptionsMenu(); + recyclerView.getAdapter().notifyDataSetChanged(); + } + + @Override + public void onError(@NotNull Throwable e) { + edit = false; + } + + @Override + public void onComplete() { + + } + }); + } + } + + private void setErrorMessageForMultiList(String headline, String message, @DrawableRes int errorResource) { + if (getActivity() == null) { + return; + } + + ((TextView) getActivity().findViewById(R.id.empty_list_view_headline)).setText(headline); + ((TextView) getActivity().findViewById(R.id.empty_list_view_text)).setText(message); + ((ImageView) getActivity().findViewById(R.id.empty_list_icon)).setImageResource(errorResource); + + getActivity().findViewById(R.id.empty_list_icon).setVisibility(View.VISIBLE); + getActivity().findViewById(R.id.empty_list_view_text).setVisibility(View.VISIBLE); + getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE); + getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE); + } + + private List createUserInfoDetails(UserProfileData userInfo) { + List result = new LinkedList<>(); + + addToList(result, + R.drawable.ic_user, + userInfo.getDisplayName(), + R.string.user_info_displayname, + Field.DISPLAYNAME, + userInfo.getDisplayNameScope()); + addToList(result, + R.drawable.ic_phone, + userInfo.getPhone(), + R.string.user_info_phone, + Field.PHONE, + userInfo.getPhoneScope()); + addToList(result, R.drawable.ic_email, userInfo.getEmail(), R.string.user_info_email, Field.EMAIL, userInfo.getEmailScope()); + addToList(result, + R.drawable.ic_map_marker, + userInfo.getAddress(), + R.string.user_info_address, + Field.ADDRESS, + userInfo.getAddressScope()); + addToList(result, + R.drawable.ic_web, + DisplayUtils.beautifyURL(userInfo.getWebsite()), + R.string.user_info_website, + Field.WEBSITE, + userInfo.getWebsiteScope()); + addToList( + result, + R.drawable.ic_twitter, + DisplayUtils.beautifyTwitterHandle(userInfo.getTwitter()), + R.string.user_info_twitter, + Field.TWITTER, + userInfo.getTwitterScope()); + + return result; + } + + private void addToList(List info, + @DrawableRes int icon, + String text, + @StringRes int contentDescriptionInt, + Field field, + Scope scope) { + info.add(new UserInfoDetailsItem(icon, text, getResources().getString(contentDescriptionInt), field, scope)); + } + + private void save() { + UserInfoAdapter adapter = (UserInfoAdapter) recyclerView.getAdapter(); + + for (UserInfoDetailsItem item : adapter.mDisplayList) { + // Text + if (!item.text.equals(userInfo.getValueByField(item.field))) { + String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); + + ncApi.setUserData( + credentials, + ApiUtils.getUrlForUserData(currentUser.getBaseUrl(), currentUser.getUserId()), + item.field.fieldName, + item.text) + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull GenericOverall userProfileOverall) { + Log.d("ProfileController", "Successfully saved: " + item.text + " as " + item.field); + + if (item.field == Field.DISPLAYNAME) { + ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(item.text); + } + } + + @Override + public void onError(@NotNull Throwable e) { + item.text = userInfo.getValueByField(item.field); + Log.e("ProfileController", "Failed to saved: " + item.text + " as " + item.field); + } + + @Override + public void onComplete() { + + } + }); + } + + // Scope + if (item.scope != userInfo.getScopeByField(item.field)) { + saveScope(item, userInfo); + } + } + } + + private void sendSelectLocalFileIntent() { + Intent intent = ImagePicker.Companion.with(getActivity()) + .galleryOnly() + .crop() + .cropSquare() + .compress(1024) + .maxResultSize(1024, 1024) + .prepareIntent(); + + startActivityForResult(intent, 1); + } + + private void showBrowserScreen(BrowserController.BrowserType browserType) { + Bundle bundle = new Bundle(); + bundle.putParcelable( + BundleKeys.INSTANCE.getKEY_BROWSER_TYPE(), + Parcels.wrap(BrowserController.BrowserType.class, browserType)); + bundle.putParcelable( + BundleKeys.INSTANCE.getKEY_USER_ENTITY(), + Parcels.wrap(UserEntity.class, currentUser)); + bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), "123"); + getRouter().pushController(RouterTransaction.with(new BrowserForAvatarController(bundle, this)) + .pushChangeHandler(new VerticalChangeHandler()) + .popChangeHandler(new VerticalChangeHandler())); + } + + public void handleAvatar(String remotePath) { + String uri = currentUser.getBaseUrl() + "/index.php/apps/files/api/v1/thumbnail/512/512/" + + Uri.encode(remotePath, "/"); + + Call downloadCall = ncApi.downloadResizedImage( + ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + uri); + + downloadCall.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body().byteStream())); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + private void saveBitmapAndPassToImagePicker(Bitmap bitmap) { + File file = null; + try { + file = File.createTempFile("avatar", "png", + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); + + + try (FileOutputStream out = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + } catch (IOException e) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (file == null) { + // TODO exception + return; + } + + Intent intent = ImagePicker.Companion.with(getActivity()) + .fileOnly() + .crop() + .cropSquare() + .compress(1024) + .maxResultSize(1024, 1024) + .prepareIntent(); + + intent.putExtra(ImagePicker.EXTRA_FILE, file); + + startActivityForResult(intent, 1); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK) { + uploadAvatar(ImagePicker.Companion.getFile(data)); + } else if (resultCode == ImagePicker.RESULT_ERROR) { + Toast.makeText(getActivity(), ImagePicker.Companion.getError(data), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getActivity(), "Task Cancelled", Toast.LENGTH_SHORT).show(); + } + } + + private void uploadAvatar(File file) { + MultipartBody.Builder builder = new MultipartBody.Builder(); + builder.setType(MultipartBody.FORM); + builder.addFormDataPart("files[]", file.getName(), RequestBody.create(MediaType.parse("image/*"), file)); + + final MultipartBody.Part filePart = MultipartBody.Part.createFormData("files[]", file.getName(), + RequestBody.create(MediaType.parse("image/jpg"), file)); + + // upload file + ncApi.uploadAvatar( + ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ApiUtils.getUrlForTempAvatar(currentUser.getBaseUrl()), + filePart) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull GenericOverall genericOverall) { + DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image)); + } + + @Override + public void onError(@NotNull Throwable e) { + Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_LONG).show(); + } + + @Override + public void onComplete() { + + } + }); + } + + public void saveScope(UserInfoDetailsItem item, UserProfileData userInfo) { + String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); + ncApi.setUserData( + credentials, + ApiUtils.getUrlForUserData(currentUser.getBaseUrl(), currentUser.getUserId()), + item.field.getScopeName(), + item.scope.getName()) + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NotNull Disposable d) { + } + + @Override + public void onNext(@NotNull GenericOverall userProfileOverall) { + Log.d("ProfileController", "Successfully saved: " + item.scope + " as " + item.field); + } + + @Override + public void onError(@NotNull Throwable e) { + item.scope = userInfo.getScopeByField(item.field); + Log.e("ProfileController", "Failed to saved: " + item.scope + " as " + item.field); + } + + @Override + public void onComplete() { + + } + }); + } + + protected static class UserInfoDetailsItem { + @DrawableRes + public int icon; + public String text; + public String hint; + private Field field; + private Scope scope; + + public UserInfoDetailsItem(@DrawableRes int icon, String text, String hint, Field field, Scope scope) { + this.icon = icon; + this.text = text; + this.hint = hint; + this.field = field; + this.scope = scope; + } + } + + public static class UserInfoAdapter extends RecyclerView.Adapter { + protected List mDisplayList; + @ColorInt + protected int mTintColor; + private final ProfileController controller; + + public static class ViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.user_info_detail_container) + protected View container; + @BindView(R.id.icon) + protected ImageView icon; + @BindView(R.id.user_info_edit_text) + protected EditText text; + @BindView(R.id.scope) + protected ImageView scope; + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } + + public UserInfoAdapter(List displayList, + @ColorInt int tintColor, + ProfileController controller) { + mDisplayList = displayList == null ? new LinkedList<>() : displayList; + mTintColor = tintColor; + this.controller = controller; + } + + public void setData(List displayList) { + mDisplayList = displayList == null ? new LinkedList<>() : displayList; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View view = inflater.inflate(R.layout.user_info_details_table_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + UserInfoDetailsItem item = mDisplayList.get(position); + + if (item.scope == null) { + holder.scope.setVisibility(View.GONE); + } else { + holder.scope.setVisibility(View.VISIBLE); + + switch (item.scope) { + case PRIVATE: + case LOCAL: + holder.scope.setImageResource(R.drawable.ic_password); + break; + case FEDERATED: + holder.scope.setImageResource(R.drawable.ic_contacts); + break; + case PUBLISHED: + holder.scope.setImageResource(R.drawable.ic_link); + break; + } + } + + holder.icon.setImageResource(item.icon); + holder.text.setText(item.text); + holder.text.setHint(item.hint); + holder.text.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mDisplayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString(); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + holder.icon.setContentDescription(item.hint); + DrawableCompat.setTint(holder.icon.getDrawable(), mTintColor); + + if (!TextUtils.isEmpty(item.text) || controller.edit) { + holder.container.setVisibility(View.VISIBLE); + holder.text.setTextColor(Color.BLACK); + + if (controller.edit && + controller.editableFields.contains(item.field.toString().toLowerCase(Locale.ROOT))) { + holder.text.setEnabled(true); + holder.text.setFocusableInTouchMode(true); + holder.text.setEnabled(true); + holder.text.setCursorVisible(true); + holder.text.setBackgroundTintList(ColorStateList.valueOf(mTintColor)); + holder.scope.setOnClickListener(v -> new ScopeDialog( + controller.getActivity(), + this, + item.field, + holder.getAdapterPosition()).show()); + holder.scope.setAlpha(1.0f); + } else { + holder.text.setEnabled(false); + holder.text.setFocusableInTouchMode(false); + holder.text.setEnabled(false); + holder.text.setCursorVisible(false); + holder.text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT)); + holder.scope.setOnClickListener(null); + holder.scope.setAlpha(0.4f); + } + } else { + holder.container.setVisibility(View.GONE); + } + } + + @Override + public int getItemCount() { + return mDisplayList.size(); + } + + public void updateScope(int position, Scope scope) { + mDisplayList.get(position).scope = scope; + notifyDataSetChanged(); + } + } + + public enum Field { + EMAIL("email", "emailScope"), + DISPLAYNAME("displayname", "displaynameScope"), + PHONE("phone", "phoneScope"), + ADDRESS("address", "addressScope"), + WEBSITE("website", "websiteScope"), + TWITTER("twitter", "twitterScope"); + + private final String fieldName; + private final String scopeName; + + Field(String fieldName, String scopeName) { + this.fieldName = fieldName; + this.scopeName = scopeName; + } + + public String getFieldName() { + return fieldName; + } + + public String getScopeName() { + return scopeName; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java index 18fd7503c..84e0ecb70 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java @@ -46,26 +46,16 @@ import android.widget.Button; import android.widget.Checkable; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.core.view.ViewCompat; -import androidx.emoji.widget.EmojiTextView; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; - import com.bluelinelabs.conductor.Controller; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; import com.bluelinelabs.logansquare.LoganSquare; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.view.SimpleDraweeView; -import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.R; @@ -110,6 +100,13 @@ import java.util.Objects; import javax.inject.Inject; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.view.ViewCompat; +import androidx.emoji.widget.EmojiTextView; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import autodagger.AutoInjector; import butterknife.BindView; import butterknife.OnClick; @@ -139,6 +136,8 @@ public class SettingsController extends BaseController { MaterialStandardPreference sourceCodeButton; @BindView(R.id.settings_version) MaterialStandardPreference versionInfo; + @BindView(R.id.avatarContainer) + RelativeLayout avatarContainer; @BindView(R.id.avatar_image) SimpleDraweeView avatarImageView; @BindView(R.id.display_name_text) @@ -563,7 +562,7 @@ public class SettingsController extends BaseController { displayNameTextView.setText(currentUser.getDisplayName()); } - loadAvatarImage(); + DisplayUtils.loadAvatarImage(currentUser, avatarImageView); profileQueryDisposable = ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser.getBaseUrl())) @@ -661,23 +660,12 @@ public class SettingsController extends BaseController { messageView.setVisibility(View.GONE); } } - } - private void loadAvatarImage() { - String avatarId; - if (!TextUtils.isEmpty(currentUser.getUserId())) { - avatarId = currentUser.getUserId(); - } else { - avatarId = currentUser.getUsername(); - } - - DraweeController draweeController = Fresco.newDraweeControllerBuilder() - .setOldController(avatarImageView.getController()) - .setAutoPlayAnimations(true) - .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(currentUser.getBaseUrl(), - avatarId, R.dimen.avatar_size_big), null)) - .build(); - avatarImageView.setController(draweeController); + avatarContainer.setOnClickListener(v -> + getRouter() + .pushController((RouterTransaction.with(new ProfileController()) + .pushChangeHandler(new HorizontalChangeHandler()) + .popChangeHandler(new HorizontalChangeHandler())))); } @Override diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java index 94e61603c..56663c057 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java @@ -21,6 +21,7 @@ package com.nextcloud.talk.dagger.modules; import android.content.Context; + import androidx.annotation.NonNull; import dagger.Module; import dagger.Provides; diff --git a/app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt b/app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt index 4766c5534..23fda533b 100644 --- a/app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/interfaces/SelectionInterface.kt @@ -24,4 +24,6 @@ interface SelectionInterface { fun toggleBrowserItemSelection(path: String) fun isPathSelected(path: String): Boolean + + fun shouldOnlySelectOneImageFile(): Boolean } diff --git a/app/src/main/java/com/nextcloud/talk/models/database/User.java b/app/src/main/java/com/nextcloud/talk/models/database/User.java index e86eb671d..1d02b0e9a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/User.java +++ b/app/src/main/java/com/nextcloud/talk/models/database/User.java @@ -222,4 +222,37 @@ public interface User extends Parcelable, Persistable, Serializable { } return ""; } + + // TODO later avatar can also be checked via user fields, for now it is in Talk capability + default boolean isAvatarEndpointAvailable() { + if (getCapabilities() != null) { + Capabilities capabilities; + try { + capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class); + return (capabilities != null && + capabilities.getSpreedCapability() != null && + capabilities.getSpreedCapability().getFeatures() != null && + capabilities.getSpreedCapability().getFeatures().contains("temp-user-avatar-api")); + } catch (IOException e) { + e.printStackTrace(); + } + } + return false; + } + + default boolean canEditScopes() { + if (getCapabilities() != null) { + Capabilities capabilities; + try { + capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class); + return (capabilities != null && + capabilities.getProvisioningCapability() != null && + capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() != null && + capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() > 1); + } catch (IOException e) { + e.printStackTrace(); + } + } + return false; + } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.java index a96787056..ca22ab4c6 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.java @@ -22,12 +22,14 @@ package com.nextcloud.talk.models.json.capabilities; import com.bluelinelabs.logansquare.annotation.JsonField; import com.bluelinelabs.logansquare.annotation.JsonObject; -import lombok.Data; + import org.parceler.Parcel; import java.util.HashMap; import java.util.List; +import lombok.Data; + @Parcel @Data @JsonObject @@ -43,4 +45,7 @@ public class Capabilities { @JsonField(name = "external") HashMap> externalCapability; + + @JsonField(name = "provisioning_api") + ProvisioningCapability provisioningCapability; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.java new file mode 100644 index 000000000..0dee10903 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.models.json.capabilities; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; + +import org.parceler.Parcel; + +import lombok.Data; + +@Parcel +@Data +@JsonObject +public class ProvisioningCapability { + @JsonField(name = "AccountPropertyScopesVersion") + Integer accountPropertyScopesVersion; +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java new file mode 100644 index 000000000..fc2bbd23f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter; +import com.nextcloud.talk.models.json.userprofile.Scope; + +public class ScopeConverter extends StringBasedTypeConverter { + @Override + public Scope getFromString(String string) { + switch (string) { + case "v2-private": + return Scope.PRIVATE; + case "v2-local": + return Scope.LOCAL; + case "v2-federated": + return Scope.FEDERATED; + case "v2-published": + return Scope.PUBLISHED; + default: + return Scope.PRIVATE; + } + } + + @Override + public String convertToString(Scope scope) { + return scope.getName(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.java b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.java new file mode 100644 index 000000000..04bd2fa28 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.java @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.models.json.userprofile; + +public enum Scope { + PRIVATE("v2-private"), + LOCAL("v2-local"), + FEDERATED("v2-federated"), + PUBLISHED("v2-published"); + + private final String name; + + Scope(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.java b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.java index 202beb7eb..518b9b188 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.java @@ -22,9 +22,13 @@ package com.nextcloud.talk.models.json.userprofile; import com.bluelinelabs.logansquare.annotation.JsonField; import com.bluelinelabs.logansquare.annotation.JsonObject; -import lombok.Data; +import com.nextcloud.talk.controllers.ProfileController; +import com.nextcloud.talk.models.json.converters.ScopeConverter; + import org.parceler.Parcel; +import lombok.Data; + @Parcel @Data @JsonObject() @@ -32,6 +36,9 @@ public class UserProfileData { @JsonField(name = "display-name") String displayName; + @JsonField(name = "displaynameScope", typeConverter = ScopeConverter.class) + Scope displayNameScope; + @JsonField(name = "displayname") String displayNameAlt; @@ -40,4 +47,69 @@ public class UserProfileData { @JsonField(name = "phone") String phone; + + @JsonField(name = "phoneScope", typeConverter = ScopeConverter.class) + Scope phoneScope; + + @JsonField(name = "email") + String email; + + @JsonField(name = "emailScope", typeConverter = ScopeConverter.class) + Scope emailScope; + + @JsonField(name = "address") + String address; + + @JsonField(name = "addressScope", typeConverter = ScopeConverter.class) + Scope addressScope; + + @JsonField(name = "twitter") + String twitter; + + @JsonField(name = "twitterScope", typeConverter = ScopeConverter.class) + Scope twitterScope; + + @JsonField(name = "website") + String website; + + @JsonField(name = "websiteScope", typeConverter = ScopeConverter.class) + Scope websiteScope; + + public String getValueByField(ProfileController.Field field) { + switch (field) { + case EMAIL: + return email; + case DISPLAYNAME: + return displayName; + case PHONE: + return phone; + case ADDRESS: + return address; + case WEBSITE: + return website; + case TWITTER: + return twitter; + default: + return ""; + } + } + + public Scope getScopeByField(ProfileController.Field field) { + switch (field) { + case EMAIL: + return emailScope; + case DISPLAYNAME: + return displayNameScope; + case PHONE: + return phoneScope; + case ADDRESS: + return addressScope; + case WEBSITE: + return websiteScope; + case TWITTER: + return twitterScope; + default: + return null; + } + } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java new file mode 100644 index 000000000..b87aac230 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java @@ -0,0 +1,39 @@ +/* + * + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.models.json.userprofile; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; +import com.nextcloud.talk.models.json.generic.GenericOCS; + +import org.parceler.Parcel; + +import java.util.ArrayList; + +import lombok.Data; + +@Parcel +@Data +@JsonObject +public class UserProfileFieldsOCS extends GenericOCS { + @JsonField(name = "data") + ArrayList data; +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.java new file mode 100644 index 000000000..0d0861856 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.java @@ -0,0 +1,36 @@ +/* + * + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.models.json.userprofile; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; + +import org.parceler.Parcel; + +import lombok.Data; + +@Parcel +@Data +@JsonObject +public class UserProfileFieldsOverall { + @JsonField(name = "ocs") + UserProfileFieldsOCS ocs; +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt new file mode 100644 index 000000000..207c3d478 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt @@ -0,0 +1,70 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2021 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.ui.dialog + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.controllers.ProfileController +import com.nextcloud.talk.models.json.userprofile.Scope + + +class ScopeDialog(con: Context, + private val userInfoAdapter: ProfileController.UserInfoAdapter, + private val field: ProfileController.Field, + private val position: Int) : + BottomSheetDialog(con) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val view = layoutInflater.inflate(R.layout.dialog_scope, null) + setContentView(view) + + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + if (field == ProfileController.Field.DISPLAYNAME || field == ProfileController.Field.EMAIL) { + findViewById(R.id.scope_private)?.visibility = View.GONE + } + + findViewById(R.id.scope_private)?.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.PRIVATE) + dismiss() + } + + findViewById(R.id.scope_local)?.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.LOCAL) + dismiss() + } + + findViewById(R.id.scope_federated)?.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.FEDERATED) + dismiss() + } + + findViewById(R.id.scope_published)?.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.PUBLISHED) + dismiss() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 43bd11f59..d7f7bd18f 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -22,9 +22,6 @@ package com.nextcloud.talk.utils; import android.net.Uri; import android.text.TextUtils; -import androidx.annotation.DimenRes; -import androidx.annotation.Nullable; - import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; @@ -33,6 +30,8 @@ import com.nextcloud.talk.models.RetrofitBucket; import java.util.HashMap; import java.util.Map; +import androidx.annotation.DimenRes; +import androidx.annotation.Nullable; import okhttp3.Credentials; public class ApiUtils { @@ -303,4 +302,12 @@ public class ApiUtils { public static String getUrlForMessageDeletion(String baseUrl, String token, String messageId) { return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token + "/" + messageId; } + + public static String getUrlForTempAvatar(String baseUrl) { + return baseUrl + ocsApiVersion + "/apps/spreed/temp-user-avatar"; + } + + public static String getUrlForUserFields(String baseUrl) { + return baseUrl + ocsApiVersion + "/cloud/user/fields"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index a0b1fc9da..7d565c356 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -78,6 +78,7 @@ import com.facebook.common.references.CloseableReference; import com.facebook.datasource.DataSource; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.controller.ControllerListener; +import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.imagepipeline.common.RotationOptions; import com.facebook.imagepipeline.core.ImagePipeline; @@ -107,6 +108,17 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.XmlRes; +import androidx.appcompat.widget.AppCompatDrawableManager; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.emoji.text.EmojiCompat; + public class DisplayUtils { private static final String TAG = "DisplayUtils"; @@ -114,6 +126,10 @@ public class DisplayUtils { private static final int INDEX_LUMINATION = 2; private static final double MAX_LIGHTNESS = 0.92; + private static final String TWITTER_HANDLE_PREFIX = "@"; + private static final String HTTP_PROTOCOL = "http://"; + private static final String HTTPS_PROTOCOL = "https://"; + public static void setClickableString(String string, String url, TextView textView) { SpannableString spannableString = new SpannableString(string); spannableString.setSpan(new ClickableSpan() { @@ -471,4 +487,68 @@ public class DisplayUtils { editText.setTextSize(16); editText.setHintTextColor(context.getResources().getColor(R.color.fontSecondaryAppbar)); } + + /** + * beautifies a given URL by removing any http/https protocol prefix. + * + * @param url to be beautified url + * @return beautified url + */ + public static String beautifyURL(@Nullable String url) { + if (TextUtils.isEmpty(url)) { + return ""; + } + + if (url.length() >= 7 && HTTP_PROTOCOL.equalsIgnoreCase(url.substring(0, 7))) { + return url.substring(HTTP_PROTOCOL.length()).trim(); + } + + if (url.length() >= 8 && HTTPS_PROTOCOL.equalsIgnoreCase(url.substring(0, 8))) { + return url.substring(HTTPS_PROTOCOL.length()).trim(); + } + + return url.trim(); + } + + /** + * beautifies a given twitter handle by prefixing it with an @ in case it is missing. + * + * @param handle to be beautified twitter handle + * @return beautified twitter handle + */ + public static String beautifyTwitterHandle(@Nullable String handle) { + if (handle != null) { + String trimmedHandle = handle.trim(); + + if (TextUtils.isEmpty(trimmedHandle)) { + return ""; + } + + if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) { + return trimmedHandle; + } else { + return TWITTER_HANDLE_PREFIX + trimmedHandle; + } + } else { + return ""; + } + } + + public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView) { + String avatarId; + if (!TextUtils.isEmpty(user.getUserId())) { + avatarId = user.getUserId(); + } else { + avatarId = user.getUsername(); + } + + DraweeController draweeController = Fresco.newDraweeControllerBuilder() + .setOldController(avatarImageView.getController()) + .setAutoPlayAnimations(true) + .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), + avatarId, R.dimen.avatar_size_big), null)) + .build(); + avatarImageView.setController(draweeController); + } } + diff --git a/app/src/main/res/drawable/ic_contacts.xml b/app/src/main/res/drawable/ic_contacts.xml new file mode 100644 index 000000000..ff82dab6d --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml new file mode 100644 index 000000000..0ed3268c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_email.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 000000000..df1072811 --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_empty_error.xml b/app/src/main/res/drawable/ic_list_empty_error.xml new file mode 100644 index 000000000..e606dd38b --- /dev/null +++ b/app/src/main/res/drawable/ic_list_empty_error.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_marker.xml b/app/src/main/res/drawable/ic_map_marker.xml new file mode 100644 index 000000000..a15914627 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/ic_password.xml b/app/src/main/res/drawable/ic_password.xml new file mode 100644 index 000000000..4ea96d2b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_password.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 000000000..374181037 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml new file mode 100644 index 000000000..9bae2778b --- /dev/null +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml new file mode 100644 index 000000000..0587b58fa --- /dev/null +++ b/app/src/main/res/drawable/ic_user.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/ic_web.xml b/app/src/main/res/drawable/ic_web.xml new file mode 100644 index 000000000..8431fcf52 --- /dev/null +++ b/app/src/main/res/drawable/ic_web.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/round_corner.xml b/app/src/main/res/drawable/round_corner.xml new file mode 100644 index 000000000..ecc82cfd1 --- /dev/null +++ b/app/src/main/res/drawable/round_corner.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/trashbin.xml b/app/src/main/res/drawable/trashbin.xml new file mode 100644 index 000000000..8f932af6a --- /dev/null +++ b/app/src/main/res/drawable/trashbin.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/upload.xml b/app/src/main/res/drawable/upload.xml new file mode 100644 index 000000000..eff81759f --- /dev/null +++ b/app/src/main/res/drawable/upload.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/controller_profile.xml b/app/src/main/res/layout/controller_profile.xml new file mode 100644 index 000000000..9ccff7dc5 --- /dev/null +++ b/app/src/main/res/layout/controller_profile.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/controller_settings.xml b/app/src/main/res/layout/controller_settings.xml index c96551fef..4f3df1037 100644 --- a/app/src/main/res/layout/controller_settings.xml +++ b/app/src/main/res/layout/controller_settings.xml @@ -43,6 +43,7 @@ android:animateLayoutChanges="true"> diff --git a/app/src/main/res/layout/dialog_scope.xml b/app/src/main/res/layout/dialog_scope.xml new file mode 100644 index 000000000..fabf72dd4 --- /dev/null +++ b/app/src/main/res/layout/dialog_scope.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/empty_list.xml b/app/src/main/res/layout/empty_list.xml new file mode 100644 index 000000000..40ad3c2c7 --- /dev/null +++ b/app/src/main/res/layout/empty_list.xml @@ -0,0 +1,64 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/user_info_details_table_item.xml b/app/src/main/res/layout/user_info_details_table_item.xml new file mode 100644 index 000000000..a0d4333be --- /dev/null +++ b/app/src/main/res/layout/user_info_details_table_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_profile.xml b/app/src/main/res/menu/menu_profile.xml new file mode 100644 index 000000000..31d4138c3 --- /dev/null +++ b/app/src/main/res/menu/menu_profile.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f97047a89..509ddcc3d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -44,4 +44,16 @@ 80dp 16dp 24dp + 16dp + 16sp + 12sp + 56dp + 24dp + 24dp + 32dp + 72dp + 72dp + 16dp + 8dp + 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c1138899..27d56164f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,7 +356,36 @@ No phone book integration due to missing permissions Chat via %s Account not found - + + Avatar + Account icon + No personal info set + Add name, picture and contact details on your profile page. + Failed to retrieve personal user information. + Phone number + E-mail + Address + Website + Twitter + Full name + folder + Loading… + Edit + Save + Upload new avatar from device + Choose avatar from cloud + Delete avatar + Private + Don\'t show via public link + Lock symbol + Local + Don\'t synchronize to servers + Federated + Only synchronize to trusted servers + Published + Synchronize to trusted serves and the global and public address book + Scope toggle + Search in %s From e8a2857a7ed15c159f6c2f4318838a38c6408fb6 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 30 Mar 2021 14:52:26 +0200 Subject: [PATCH 02/16] Updated strings / icon to match server Signed-off-by: tobiasKaminsky --- app/src/main/res/layout/dialog_scope.xml | 2 +- app/src/main/res/values/strings.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/dialog_scope.xml b/app/src/main/res/layout/dialog_scope.xml index fabf72dd4..3fa9ab8c8 100644 --- a/app/src/main/res/layout/dialog_scope.xml +++ b/app/src/main/res/layout/dialog_scope.xml @@ -38,7 +38,7 @@ android:layout_height="32dp" android:layout_gravity="center_vertical" android:contentDescription="@string/lock_symbol" - app:srcCompat="@drawable/ic_password" /> + app:srcCompat="@drawable/ic_call_black_24dp" /> Choose avatar from cloud Delete avatar Private - Don\'t show via public link + Only visible to people matched via phone number integration through Talk on mobile Lock symbol Local - Don\'t synchronize to servers + Only visible to people on this instance and guests Federated Only synchronize to trusted servers Published - Synchronize to trusted serves and the global and public address book + Synchronize to trusted servers and the global and public address book Scope toggle From 1fec3e7635c75c1cf42455ad57cdad37dbfc6de6 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 30 Mar 2021 17:34:51 +0200 Subject: [PATCH 03/16] modify descriptions for phone number integration (avoid the word "phonebook" because then people would expect that numbers are used in the nc contact app) Signed-off-by: Marcel Hibbe --- app/src/main/res/values/strings.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 895e670a6..4019613cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,14 +346,15 @@ phone_book_integration - Match contacts based on phone number to integrate Talk shortcut in phone book - Phone book integration + Match contacts based on phone number to integrate Talk + shortcut into system contacts app + Phone number integration Phone number You can set your phone number so other users will be able to find you Invalid phone number Phone number set successfully - No phone book integration due to missing permissions + No phone number integration due to missing permissions Chat via %s Account not found From a57253e0df482dc6687e632afcaf7d996c7757e5 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 7 Apr 2021 13:13:36 +0200 Subject: [PATCH 04/16] Fix bugs found by Marcel Signed-off-by: tobiasKaminsky --- .../talk/controllers/ProfileController.java | 94 ++++++++++++++----- app/src/main/res/drawable/ic_password.xml | 16 ++++ app/src/main/res/drawable/upload.xml | 17 +++- .../layout/user_info_details_table_item.xml | 6 +- app/src/main/res/values/strings.xml | 21 ++--- 5 files changed, 111 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index 3fa3733c2..ad22cac76 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -111,6 +111,7 @@ public class ProfileController extends BaseController { private UserEntity currentUser; private boolean edit = false; private RecyclerView recyclerView; + private UserInfoAdapter adapter; private UserProfileData userInfo; private ArrayList editableFields = new ArrayList<>(); @@ -143,6 +144,12 @@ public class ProfileController extends BaseController { super.onPrepareOptionsMenu(menu); menu.findItem(R.id.edit).setVisible(editableFields.size() > 0); + + if (edit) { + menu.findItem(R.id.edit).setTitle(R.string.save); + } else { + menu.findItem(R.id.edit).setTitle(R.string.edit); + } } @Override @@ -158,12 +165,16 @@ public class ProfileController extends BaseController { if (edit) { item.setTitle(R.string.save); + getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE); + getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE); + if (currentUser.isAvatarEndpointAvailable()) { // TODO later avatar can also be checked via user fields, for now it is in Talk capability getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.VISIBLE); } - ncApi.getEditableUserProfileFields(ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ncApi.getEditableUserProfileFields( + ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), ApiUtils.getUrlForUserFields(currentUser.getBaseUrl())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -175,7 +186,7 @@ public class ProfileController extends BaseController { @Override public void onNext(@NotNull UserProfileFieldsOverall userProfileFieldsOverall) { editableFields = userProfileFieldsOverall.getOcs().getData(); - recyclerView.getAdapter().notifyDataSetChanged(); + adapter.notifyDataSetChanged(); } @Override @@ -191,9 +202,14 @@ public class ProfileController extends BaseController { } else { item.setTitle(R.string.edit); getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE); + + if (adapter.filteredDisplayList.size() == 0) { + getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE); + getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE); + } } - recyclerView.getAdapter().notifyDataSetChanged(); + adapter.notifyDataSetChanged(); return true; @@ -207,9 +223,8 @@ public class ProfileController extends BaseController { super.onAttach(view); recyclerView = getActivity().findViewById(R.id.userinfo_list); - recyclerView.setAdapter(new UserInfoAdapter(null, - getActivity().getResources().getColor(R.color.colorPrimary), - this)); + adapter = new UserInfoAdapter(null, getActivity().getResources().getColor(R.color.colorPrimary), this); + recyclerView.setAdapter(adapter); recyclerView.setItemViewCacheSize(20); currentUser = userUtils.getCurrentUser(); @@ -291,7 +306,12 @@ public class ProfileController extends BaseController { ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(userInfo.getDisplayName()); } - if (TextUtils.isEmpty(userInfo.getPhone()) && + getActivity().findViewById(R.id.loading_content).setVisibility(View.VISIBLE); + + adapter.setData(createUserInfoDetails(userInfo)); + + if (TextUtils.isEmpty(userInfo.getDisplayName()) && + TextUtils.isEmpty(userInfo.getPhone()) && TextUtils.isEmpty(userInfo.getEmail()) && TextUtils.isEmpty(userInfo.getAddress()) && TextUtils.isEmpty(userInfo.getTwitter()) && @@ -305,15 +325,8 @@ public class ProfileController extends BaseController { getActivity().getString(R.string.userinfo_no_info_headline), getActivity().getString(R.string.userinfo_no_info_text), R.drawable.ic_user); } else { - getActivity().findViewById(R.id.loading_content).setVisibility(View.VISIBLE); getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE); - RecyclerView recyclerView = getActivity().findViewById(R.id.userinfo_list); - if (recyclerView.getAdapter() instanceof UserInfoAdapter) { - UserInfoAdapter adapter = ((UserInfoAdapter) recyclerView.getAdapter()); - adapter.setData(createUserInfoDetails(userInfo)); - } - getActivity().findViewById(R.id.loading_content).setVisibility(View.GONE); getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE); } @@ -334,7 +347,7 @@ public class ProfileController extends BaseController { editableFields = userProfileFieldsOverall.getOcs().getData(); getActivity().invalidateOptionsMenu(); - recyclerView.getAdapter().notifyDataSetChanged(); + adapter.notifyDataSetChanged(); } @Override @@ -414,9 +427,7 @@ public class ProfileController extends BaseController { } private void save() { - UserInfoAdapter adapter = (UserInfoAdapter) recyclerView.getAdapter(); - - for (UserInfoDetailsItem item : adapter.mDisplayList) { + for (UserInfoDetailsItem item : adapter.displayList) { // Text if (!item.text.equals(userInfo.getValueByField(item.field))) { String credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); @@ -460,6 +471,8 @@ public class ProfileController extends BaseController { if (item.scope != userInfo.getScopeByField(item.field)) { saveScope(item, userInfo); } + + adapter.updateFilteredList(); } } @@ -643,7 +656,8 @@ public class ProfileController extends BaseController { } public static class UserInfoAdapter extends RecyclerView.Adapter { - protected List mDisplayList; + protected List displayList; + protected List filteredDisplayList = new LinkedList<>(); @ColorInt protected int mTintColor; private final ProfileController controller; @@ -667,16 +681,31 @@ public class ProfileController extends BaseController { public UserInfoAdapter(List displayList, @ColorInt int tintColor, ProfileController controller) { - mDisplayList = displayList == null ? new LinkedList<>() : displayList; + this.displayList = displayList == null ? new LinkedList<>() : displayList; mTintColor = tintColor; this.controller = controller; } public void setData(List displayList) { - mDisplayList = displayList == null ? new LinkedList<>() : displayList; + this.displayList = displayList == null ? new LinkedList<>() : displayList; + + updateFilteredList(); + notifyDataSetChanged(); } + public void updateFilteredList() { + filteredDisplayList.clear(); + + if (displayList != null) { + for (UserInfoDetailsItem item : displayList) { + if (!TextUtils.isEmpty(item.text)) { + filteredDisplayList.add(item); + } + } + } + } + @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -687,7 +716,14 @@ public class ProfileController extends BaseController { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - UserInfoDetailsItem item = mDisplayList.get(position); + UserInfoDetailsItem item; + + if (controller.edit) { + item = displayList.get(position); + } else { + item = filteredDisplayList.get(position); + } + if (item.scope == null) { holder.scope.setVisibility(View.GONE); @@ -719,7 +755,11 @@ public class ProfileController extends BaseController { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - mDisplayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString(); + if (controller.edit) { + displayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString(); + } else { + filteredDisplayList.get(holder.getAdapterPosition()).text = holder.text.getText().toString(); + } } @Override @@ -764,11 +804,15 @@ public class ProfileController extends BaseController { @Override public int getItemCount() { - return mDisplayList.size(); + if (controller.edit) { + return displayList.size(); + } else { + return filteredDisplayList.size(); + } } public void updateScope(int position, Scope scope) { - mDisplayList.get(position).scope = scope; + displayList.get(position).scope = scope; notifyDataSetChanged(); } } diff --git a/app/src/main/res/drawable/ic_password.xml b/app/src/main/res/drawable/ic_password.xml index 4ea96d2b5..622286aca 100644 --- a/app/src/main/res/drawable/ic_password.xml +++ b/app/src/main/res/drawable/ic_password.xml @@ -1,3 +1,19 @@ + + + tools:ignore="LabelFor" + tools:text="+49 123 456 789 12" /> Delete conversation Delete Please confirm your intent to remove the conversation. - If you delete the conversation, it will also be - deleted for %1$s. + If you delete the conversation, it will also be deleted for %1$s. If you delete the conversation, it will also be deleted for all other participants. New conversation @@ -198,8 +197,7 @@ Incoming call from Guest New public conversation - Public conversations let you invite people from outside through a - specially crafted link. + Public conversations let you invite people from outside through a specially crafted link. No response in 45 seconds, tap to try again Reconnecting… Currently offline, please check your connectivity @@ -218,13 +216,11 @@ Mute calls Incoming calls will be silenced Important conversation - Notifications in this conversation will override - Do Not Disturb settings + Notifications in this conversation will override Do Not Disturb settings Sorry, something went wrong! - Target server does not support joining public conversations via mobile - phones. You may attempt to join the conversation via web browser. + Target server does not support joining public conversations via mobile phones. You may attempt to join the conversation via web browser. OK, all done! OK Conversation name @@ -346,12 +342,10 @@ phone_book_integration - Match contacts based on phone number to integrate Talk - shortcut into system contacts app + Match contacts based on phone number to integrate Talk shortcut into system contacts app Phone number integration Phone number - You can set your phone number so - other users will be able to find you + You can set your phone number so other users will be able to find you Invalid phone number Phone number set successfully No phone number integration due to missing permissions @@ -391,8 +385,7 @@ Search in %s - M3.27,4.27L19.74,20.74 + M3.27,4.27L19.74,20.74 999+ Open main menu From 6fb5d3f9bbd945c027ca04714d70626aef365ed4 Mon Sep 17 00:00:00 2001 From: Tobias Kaminsky Date: Wed, 7 Apr 2021 17:09:36 +0200 Subject: [PATCH 05/16] Fixed some spotbugs, excluded some bug categories to be consistent with Files Signed-off-by: tobiasKaminsky --- .../talk/controllers/ProfileController.java | 1 + .../json/userprofile/UserProfileFieldsOCS.java | 2 ++ findbugs-filter.xml | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index ad22cac76..49935537e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -523,6 +523,7 @@ public class ProfileController extends BaseController { }); } + @SuppressWarnings({"IOI_USE_OF_FILE_STREAM_CONSTRUCTORS"}) // only possible with API26 private void saveBitmapAndPassToImagePicker(Bitmap bitmap) { File file = null; try { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java index b87aac230..26e60ee5c 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.java @@ -29,7 +29,9 @@ import org.parceler.Parcel; import java.util.ArrayList; import lombok.Data; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode(callSuper = true) @Parcel @Data @JsonObject diff --git a/findbugs-filter.xml b/findbugs-filter.xml index 585711f9f..514a0e3a5 100644 --- a/findbugs-filter.xml +++ b/findbugs-filter.xml @@ -23,9 +23,23 @@ - + - + + + + + + + + + + + + + + + From 43108661e9a7162c94e4d3d2df4219d4a6a34075 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 7 Apr 2021 17:39:09 +0200 Subject: [PATCH 06/16] Clear cached avatar Signed-off-by: tobiasKaminsky --- .../talk/controllers/ProfileController.java | 10 ++++++---- .../talk/controllers/SettingsController.java | 2 +- .../com/nextcloud/talk/utils/DisplayUtils.java | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index 49935537e..f90ed6380 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -245,8 +245,10 @@ public class ProfileController extends BaseController { @Override public void onNext(@NotNull GenericOverall genericOverall) { - DisplayUtils.loadAvatarImage(currentUser, - getActivity().findViewById(R.id.avatar_image)); + DisplayUtils.loadAvatarImage( + currentUser, + getActivity().findViewById(R.id.avatar_image), + true); } @Override @@ -300,7 +302,7 @@ public class ProfileController extends BaseController { .findViewById(R.id.userinfo_baseurl)) .setText(Uri.parse(currentUser.getBaseUrl()).getHost()); - DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image)); + DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image), false); if (!TextUtils.isEmpty(userInfo.getDisplayName())) { ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(userInfo.getDisplayName()); @@ -591,7 +593,7 @@ public class ProfileController extends BaseController { @Override public void onNext(@NotNull GenericOverall genericOverall) { - DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image)); + DisplayUtils.loadAvatarImage(currentUser, getActivity().findViewById(R.id.avatar_image), true); } @Override diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java index 84e0ecb70..12e9fb678 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java @@ -562,7 +562,7 @@ public class SettingsController extends BaseController { displayNameTextView.setText(currentUser.getDisplayName()); } - DisplayUtils.loadAvatarImage(currentUser, avatarImageView); + DisplayUtils.loadAvatarImage(currentUser, avatarImageView, false); profileQueryDisposable = ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser.getBaseUrl())) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 7d565c356..dffa728b5 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -534,7 +534,7 @@ public class DisplayUtils { } } - public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView) { + public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) { String avatarId; if (!TextUtils.isEmpty(user.getUserId())) { avatarId = user.getUserId(); @@ -542,11 +542,21 @@ public class DisplayUtils { avatarId = user.getUsername(); } + // clear cache + if (deleteCache) { + String avatarString = ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), avatarId, R.dimen.avatar_size_big); + Uri avatarUri = Uri.parse(avatarString); + + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + imagePipeline.evictFromMemoryCache(avatarUri); + imagePipeline.evictFromDiskCache(avatarUri); + imagePipeline.evictFromCache(avatarUri); + } + DraweeController draweeController = Fresco.newDraweeControllerBuilder() .setOldController(avatarImageView.getController()) .setAutoPlayAnimations(true) - .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), - avatarId, R.dimen.avatar_size_big), null)) + .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarString, null)) .build(); avatarImageView.setController(draweeController); } From d497cdd895f7b29123237a70bc944b6d52c011cb Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 00:08:20 +0200 Subject: [PATCH 07/16] move variable declaration up to fix compile issue Signed-off-by: Andy Scherzinger --- app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index dffa728b5..a580336e9 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -533,7 +533,7 @@ public class DisplayUtils { return ""; } } - + public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) { String avatarId; if (!TextUtils.isEmpty(user.getUserId())) { @@ -542,9 +542,10 @@ public class DisplayUtils { avatarId = user.getUsername(); } + String avatarString = ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), avatarId, R.dimen.avatar_size_big); + // clear cache if (deleteCache) { - String avatarString = ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), avatarId, R.dimen.avatar_size_big); Uri avatarUri = Uri.parse(avatarString); ImagePipeline imagePipeline = Fresco.getImagePipeline(); From e224e093b04bc018bebaf6c110db1f0d025be943 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 00:09:16 +0200 Subject: [PATCH 08/16] extracted shimmer element for user detail items Signed-off-by: Andy Scherzinger --- .../main/res/layout/controller_profile.xml | 84 +------------------ .../user_info_details_table_item_shimmer.xml | 40 +++++++++ 2 files changed, 44 insertions(+), 80 deletions(-) create mode 100644 app/src/main/res/layout/user_info_details_table_item_shimmer.xml diff --git a/app/src/main/res/layout/controller_profile.xml b/app/src/main/res/layout/controller_profile.xml index 9ccff7dc5..ba793c4b1 100644 --- a/app/src/main/res/layout/controller_profile.xml +++ b/app/src/main/res/layout/controller_profile.xml @@ -130,89 +130,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/user_info_details_table_item_shimmer.xml b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml new file mode 100644 index 000000000..cf86ee9ac --- /dev/null +++ b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml @@ -0,0 +1,40 @@ + + + + + + + + + From c461ddca5bfab06061cd8738dc1e4d90f619bc7a Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 09:40:30 +0200 Subject: [PATCH 09/16] respect light/dark theming Signed-off-by: Andy Scherzinger --- .../talk/controllers/ProfileController.java | 8 +++++- .../main/res/drawable-night/ic_contacts.xml | 9 +++++++ app/src/main/res/drawable-night/ic_link.xml | 9 +++++++ .../main/res/drawable-night/ic_password.xml | 25 +++++++++++++++++++ .../layout/user_info_details_table_item.xml | 8 +++--- .../user_info_details_table_item_shimmer.xml | 4 +-- 6 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable-night/ic_contacts.xml create mode 100644 app/src/main/res/drawable-night/ic_link.xml create mode 100644 app/src/main/res/drawable-night/ic_password.xml diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index f90ed6380..df563cb07 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -83,6 +83,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; @@ -776,7 +777,12 @@ public class ProfileController extends BaseController { if (!TextUtils.isEmpty(item.text) || controller.edit) { holder.container.setVisibility(View.VISIBLE); - holder.text.setTextColor(Color.BLACK); + if (controller.getActivity() != null) { + holder.text.setTextColor(ContextCompat.getColor( + controller.getActivity(), + R.color.conversation_item_header) + ); + } if (controller.edit && controller.editableFields.contains(item.field.toString().toLowerCase(Locale.ROOT))) { diff --git a/app/src/main/res/drawable-night/ic_contacts.xml b/app/src/main/res/drawable-night/ic_contacts.xml new file mode 100644 index 000000000..099f2cfdb --- /dev/null +++ b/app/src/main/res/drawable-night/ic_contacts.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_link.xml b/app/src/main/res/drawable-night/ic_link.xml new file mode 100644 index 000000000..f21fc4fb5 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_password.xml b/app/src/main/res/drawable-night/ic_password.xml new file mode 100644 index 000000000..9a0fe47fa --- /dev/null +++ b/app/src/main/res/drawable-night/ic_password.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/layout/user_info_details_table_item.xml b/app/src/main/res/layout/user_info_details_table_item.xml index cd8052852..8ac631253 100644 --- a/app/src/main/res/layout/user_info_details_table_item.xml +++ b/app/src/main/res/layout/user_info_details_table_item.xml @@ -29,7 +29,7 @@ android:id="@+id/icon" android:layout_width="@dimen/iconized_single_line_item_icon_size" android:layout_height="@dimen/iconized_single_line_item_icon_size" - android:layout_marginStart="@dimen/user_info_icon_horizontal_margin" + android:layout_marginStart="@dimen/standard_margin" android:contentDescription="@string/account_icon" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -40,13 +40,12 @@ android:id="@+id/user_info_edit_text" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/user_info_icon_horizontal_margin" + android:layout_marginStart="@dimen/standard_margin" android:layout_marginEnd="@dimen/standard_margin" android:autofillHints="none" android:ellipsize="end" android:inputType="text" android:maxLines="1" - android:textAppearance="?android:attr/textAppearanceListItem" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scope" app:layout_constraintStart_toEndOf="@id/icon" @@ -58,8 +57,7 @@ android:id="@+id/scope" android:layout_width="24dp" android:layout_height="24dp" - android:layout_marginStart="@dimen/user_info_icon_horizontal_margin" - android:layout_marginEnd="@dimen/standard_double_margin" + android:layout_marginEnd="@dimen/standard_margin" android:contentDescription="@string/scope_toggle" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/user_info_details_table_item_shimmer.xml b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml index cf86ee9ac..3482d1199 100644 --- a/app/src/main/res/layout/user_info_details_table_item_shimmer.xml +++ b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml @@ -27,14 +27,14 @@ android:layout_width="@dimen/iconized_single_line_item_icon_size" android:layout_height="@dimen/iconized_single_line_item_icon_size" android:layout_gravity="center_vertical" - android:layout_marginStart="@dimen/user_info_icon_horizontal_margin" + android:layout_marginStart="@dimen/standard_margin" app:corners="100" /> From b1a60b6cf1ffce1e460a5a0d58147772f705d2b0 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 09:47:16 +0200 Subject: [PATCH 10/16] use proper logging API Signed-off-by: Andy Scherzinger --- .../talk/controllers/ProfileController.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index df563cb07..8a0ab7e1e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -104,6 +104,8 @@ import retrofit2.Response; @AutoInjector(NextcloudTalkApplication.class) public class ProfileController extends BaseController { + private static final String TAG = ProfileController.class.getSimpleName(); + @Inject NcApi ncApi; @@ -450,7 +452,7 @@ public class ProfileController extends BaseController { @Override public void onNext(@NotNull GenericOverall userProfileOverall) { - Log.d("ProfileController", "Successfully saved: " + item.text + " as " + item.field); + Log.d(TAG, "Successfully saved: " + item.text + " as " + item.field); if (item.field == Field.DISPLAYNAME) { ((TextView) getActivity().findViewById(R.id.userinfo_fullName)).setText(item.text); @@ -460,7 +462,7 @@ public class ProfileController extends BaseController { @Override public void onError(@NotNull Throwable e) { item.text = userInfo.getValueByField(item.field); - Log.e("ProfileController", "Failed to saved: " + item.text + " as " + item.field); + Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e); } @Override @@ -537,10 +539,10 @@ public class ProfileController extends BaseController { try (FileOutputStream out = new FileOutputStream(file)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Error compressing bitmap", e); } } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Error creating temporary avatar image", e); } if (file == null) { @@ -626,13 +628,13 @@ public class ProfileController extends BaseController { @Override public void onNext(@NotNull GenericOverall userProfileOverall) { - Log.d("ProfileController", "Successfully saved: " + item.scope + " as " + item.field); + Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field); } @Override public void onError(@NotNull Throwable e) { item.scope = userInfo.getScopeByField(item.field); - Log.e("ProfileController", "Failed to saved: " + item.scope + " as " + item.field); + Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e); } @Override From a6c05cabfff64cbb7aa3374434040072aa37c3bd Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 10:18:46 +0200 Subject: [PATCH 11/16] proper user info item text size Signed-off-by: Andy Scherzinger --- app/src/main/res/layout/user_info_details_table_item.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/user_info_details_table_item.xml b/app/src/main/res/layout/user_info_details_table_item.xml index 8ac631253..563d49bfa 100644 --- a/app/src/main/res/layout/user_info_details_table_item.xml +++ b/app/src/main/res/layout/user_info_details_table_item.xml @@ -46,6 +46,7 @@ android:ellipsize="end" android:inputType="text" android:maxLines="1" + android:textSize="16sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scope" app:layout_constraintStart_toEndOf="@id/icon" From 7de3b1e2942a153e2a288c8ba8cfe25db8647366 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 10:19:26 +0200 Subject: [PATCH 12/16] optimize bottom sheet layout and make it dark/light theme aware Signed-off-by: Andy Scherzinger --- app/src/main/res/drawable-night/ic_call.xml | 29 ++++++ app/src/main/res/drawable/ic_call.xml | 29 ++++++ app/src/main/res/layout/dialog_scope.xml | 106 +++++++++++--------- 3 files changed, 119 insertions(+), 45 deletions(-) create mode 100644 app/src/main/res/drawable-night/ic_call.xml create mode 100644 app/src/main/res/drawable/ic_call.xml diff --git a/app/src/main/res/drawable-night/ic_call.xml b/app/src/main/res/drawable-night/ic_call.xml new file mode 100644 index 000000000..bc827fa62 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_call.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_call.xml b/app/src/main/res/drawable/ic_call.xml new file mode 100644 index 000000000..3dd2ef2d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_call.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_scope.xml b/app/src/main/res/layout/dialog_scope.xml index 3fa9ab8c8..8ea9068b3 100644 --- a/app/src/main/res/layout/dialog_scope.xml +++ b/app/src/main/res/layout/dialog_scope.xml @@ -2,7 +2,9 @@ ~ Nextcloud Talk application ~ ~ @author Tobias Kaminsky + ~ @author Andy Scherzinger ~ Copyright (C) 2021 Tobias Kaminsky + ~ Copyright (C) 2021 Andy Scherzinger ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by @@ -22,43 +24,51 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/bg_bottom_sheet" android:orientation="vertical" - android:paddingTop="8dp" - android:paddingBottom="8dp"> + android:paddingStart="@dimen/standard_padding" + android:paddingTop="@dimen/standard_half_padding" + android:paddingEnd="@dimen/standard_padding" + android:paddingBottom="@dimen/standard_half_padding"> + android:orientation="horizontal" + android:paddingTop="@dimen/standard_half_padding" + android:paddingBottom="@dimen/standard_half_padding"> + app:srcCompat="@drawable/ic_call" /> + android:text="@string/scope_private_description" + android:textColor="@color/textColorMaxContrast" + android:textSize="14sp" /> @@ -68,36 +78,38 @@ android:id="@+id/scope_local" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/standard_margin" - android:layout_marginTop="@dimen/standard_half_margin" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingTop="@dimen/standard_half_padding" + android:paddingBottom="@dimen/standard_half_padding"> + android:text="@string/scope_local_description" + android:textColor="@color/textColorMaxContrast" + android:textSize="14sp" /> @@ -107,36 +119,38 @@ android:id="@+id/scope_federated" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/standard_margin" - android:layout_marginTop="@dimen/standard_half_margin" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingTop="@dimen/standard_half_padding" + android:paddingBottom="@dimen/standard_half_padding"> + android:text="@string/scope_federated_description" + android:textColor="@color/textColorMaxContrast" + android:textSize="14sp" /> @@ -146,36 +160,38 @@ android:id="@+id/scope_published" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/standard_margin" - android:layout_marginTop="@dimen/standard_half_margin" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingTop="@dimen/standard_half_padding" + android:paddingBottom="@dimen/standard_half_padding"> + android:text="@string/scope_published_description" + android:textColor="@color/textColorMaxContrast" + android:textSize="14sp" /> From 208e96e1c0daba0dca8265adb7735662f9ee6b99 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 8 Apr 2021 11:16:23 +0200 Subject: [PATCH 13/16] remove unused resources Signed-off-by: Andy Scherzinger --- .../res/layout/rv_item_conversation_with_last_message.xml | 2 +- app/src/main/res/layout/user_info_details_table_item.xml | 6 +++--- app/src/main/res/values/dimens.xml | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/rv_item_conversation_with_last_message.xml b/app/src/main/res/layout/rv_item_conversation_with_last_message.xml index 949c7b6c9..aa8cc7641 100644 --- a/app/src/main/res/layout/rv_item_conversation_with_last_message.xml +++ b/app/src/main/res/layout/rv_item_conversation_with_last_message.xml @@ -129,7 +129,7 @@ android:includeFontPadding="false" android:maxLines="1" android:textColor="@color/conversation_item_header" - android:textSize="16sp" + android:textSize="@dimen/two_line_primary_text_size" tools:text="Best conversation" /> diff --git a/app/src/main/res/layout/user_info_details_table_item.xml b/app/src/main/res/layout/user_info_details_table_item.xml index 563d49bfa..eac183a0e 100644 --- a/app/src/main/res/layout/user_info_details_table_item.xml +++ b/app/src/main/res/layout/user_info_details_table_item.xml @@ -46,7 +46,7 @@ android:ellipsize="end" android:inputType="text" android:maxLines="1" - android:textSize="16sp" + android:textSize="@dimen/two_line_primary_text_size" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scope" app:layout_constraintStart_toEndOf="@id/icon" @@ -56,8 +56,8 @@ 24dp 16dp 16sp - 12sp 56dp 24dp - 24dp 32dp 72dp 72dp From ea50cf60e58823ba2432137e92c176bce8812bdd Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 8 Apr 2021 14:15:27 +0200 Subject: [PATCH 14/16] Upon error on saving, revert value Signed-off-by: tobiasKaminsky --- .../com/nextcloud/talk/controllers/ProfileController.java | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 8 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index 8a0ab7e1e..df4769a0e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -462,6 +462,13 @@ public class ProfileController extends BaseController { @Override public void onError(@NotNull Throwable e) { item.text = userInfo.getValueByField(item.field); + Toast.makeText(getApplicationContext(), + String.format(getResources().getString(R.string.failed_to_save), + item.text, + item.field), + Toast.LENGTH_LONG).show(); + adapter.updateFilteredList(); + adapter.notifyDataSetChanged(); Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 298edb026..f317816bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -388,4 +388,5 @@ M3.27,4.27L19.74,20.74 999+ Open main menu + Failed to save %1$s as %2$s From 983509396d68f10c9a01ec86c64e20898245d094 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 8 Apr 2021 15:07:18 +0200 Subject: [PATCH 15/16] show only fieldname in toast when saving failed Signed-off-by: Marcel Hibbe --- .../java/com/nextcloud/talk/controllers/ProfileController.java | 1 - app/src/main/res/values/strings.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index df4769a0e..62b2a9b72 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java @@ -464,7 +464,6 @@ public class ProfileController extends BaseController { item.text = userInfo.getValueByField(item.field); Toast.makeText(getApplicationContext(), String.format(getResources().getString(R.string.failed_to_save), - item.text, item.field), Toast.LENGTH_LONG).show(); adapter.updateFilteredList(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f317816bc..8861bbd75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -388,5 +388,5 @@ M3.27,4.27L19.74,20.74 999+ Open main menu - Failed to save %1$s as %2$s + Failed to save %1$s From 4777e8362d48f1adf6a5e4f2db72177cf01fdb0a Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 8 Apr 2021 15:12:10 +0200 Subject: [PATCH 16/16] add profile information and privacy settings to changelog Signed-off-by: Marcel Hibbe --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25448aac7..1ff2ea1a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security ## [UNRELEASED] ### Added +- edit profile information and privacy settings ### Changed - improve conversation list design and dark/light theming (@AndyScherzinger)