Add user profile, allow to edit it, if server supports it

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2021-03-22 16:57:30 +01:00
parent d38eec4a93
commit 4f0923ba0d
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
44 changed files with 2334 additions and 93 deletions

View file

@ -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'

View file

@ -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">

View file

@ -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<ChatOverallSingleMessage> deleteChatMessage(@Header("Authorization") String authorization,
@Url String url);
@DELETE
Observable<GenericOverall> deleteAvatar(@Header("Authorization") String authorization, @Url String url);
@Multipart
@POST
Observable<GenericOverall> uploadAvatar(@Header("Authorization") String authorization,
@Url String url,
@Part MultipartBody.Part attachment);
@GET
Observable<UserProfileFieldsOverall> getEditableUserProfileFields(@Header("Authorization") String authorization,
@Url String url);
@GET
Call<ResponseBody> downloadResizedImage(@Header("Authorization") String authorization,
@Url String url);
}

View file

@ -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<BrowserFileItem.ViewHolder> implements IFilterable<String> {
@Inject
@ -125,6 +130,16 @@ public class BrowserFileItem extends AbstractFlexibleItem<BrowserFileItem.ViewHo
holder.fileFavoriteImageView.setVisibility(View.GONE);
}
if (selectionInterface.shouldOnlySelectOneImageFile()) {
if (browserFile.isFile && browserFile.mimeType.startsWith("image/")) {
holder.selectFileCheckbox.setVisibility(View.VISIBLE);
} else {
holder.selectFileCheckbox.setVisibility(View.GONE);
}
} else {
holder.selectFileCheckbox.setVisibility(View.VISIBLE);
}
holder.fileIconImageView.getHierarchy().setPlaceholderImage(context.getDrawable(DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(browserFile.getMimeType())));
if (browserFile.isHasPreview()) {

View file

@ -23,15 +23,13 @@ package com.nextcloud.talk.components.filebrowser.controllers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.*;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import autodagger.AutoInjector;
import butterknife.BindView;
import butterknife.OnClick;
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 com.google.android.material.bottomnavigation.BottomNavigationItemView;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
@ -43,27 +41,39 @@ import com.nextcloud.talk.components.filebrowser.operations.DavListing;
import com.nextcloud.talk.components.filebrowser.operations.ListingAbstractClass;
import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.interfaces.SelectionInterface;
import com.nextcloud.talk.jobs.ShareOperationWorker;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import org.jetbrains.annotations.NotNull;
import org.parceler.Parcel;
import org.parceler.Parcels;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import autodagger.AutoInjector;
import butterknife.BindView;
import butterknife.OnClick;
import eu.davidea.fastscroller.FastScroller;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
import eu.davidea.flexibleadapter.items.IFlexible;
import okhttp3.OkHttpClient;
import org.parceler.Parcel;
import org.parceler.Parcels;
import javax.inject.Inject;
import java.io.File;
import java.util.*;
@AutoInjector(NextcloudTalkApplication.class)
public class BrowserController extends BaseController implements ListingInterface,
public abstract class BrowserController extends BaseController implements ListingInterface,
FlexibleAdapter.OnItemClickListener, SelectionInterface {
private final Set<String> selectedPaths;
protected final Set<String> 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<String> iterator = selectedPaths.iterator();
List<String> 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,

View file

@ -0,0 +1,59 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -0,0 +1,80 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> iterator = selectedPaths.iterator();
List<String> 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;
}
}

View file

@ -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<BrowserController.BrowserType>(browserType))
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(conversationUser))
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
router.pushController(RouterTransaction.with(BrowserController(bundle))
router.pushController(RouterTransaction.with(BrowserForSharingController(bundle))
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler()))
}

View file

@ -0,0 +1,800 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<UserProfileFieldsOverall>() {
@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<GenericOverall>() {
@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<UserProfileOverall>() {
@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<UserProfileFieldsOverall>() {
@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<UserInfoDetailsItem> createUserInfoDetails(UserProfileData userInfo) {
List<UserInfoDetailsItem> 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<UserInfoDetailsItem> 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<GenericOverall>() {
@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<ResponseBody> downloadCall = ncApi.downloadResizedImage(
ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
uri);
downloadCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body().byteStream()));
}
@Override
public void onFailure(@NonNull Call<ResponseBody> 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<GenericOverall>() {
@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<GenericOverall>() {
@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<UserInfoAdapter.ViewHolder> {
protected List<UserInfoDetailsItem> 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<UserInfoDetailsItem> displayList,
@ColorInt int tintColor,
ProfileController controller) {
mDisplayList = displayList == null ? new LinkedList<>() : displayList;
mTintColor = tintColor;
this.controller = controller;
}
public void setData(List<UserInfoDetailsItem> 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;
}
}
}

View file

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

View file

@ -21,6 +21,7 @@
package com.nextcloud.talk.dagger.modules;
import android.content.Context;
import androidx.annotation.NonNull;
import dagger.Module;
import dagger.Provides;

View file

@ -24,4 +24,6 @@ interface SelectionInterface {
fun toggleBrowserItemSelection(path: String)
fun isPathSelected(path: String): Boolean
fun shouldOnlySelectOneImageFile(): Boolean
}

View file

@ -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;
}
}

View file

@ -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<String, List<String>> externalCapability;
@JsonField(name = "provisioning_api")
ProvisioningCapability provisioningCapability;
}

View file

@ -0,0 +1,36 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -0,0 +1,47 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Scope> {
@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();
}
}

View file

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,39 @@
/*
*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> data;
}

View file

@ -0,0 +1,36 @@
/*
*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}

View file

@ -0,0 +1,70 @@
/*
* Nextcloud Talk application
*
* @author Tobias Kaminsky
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<LinearLayout>(R.id.scope_private)?.visibility = View.GONE
}
findViewById<LinearLayout>(R.id.scope_private)?.setOnClickListener {
userInfoAdapter.updateScope(position, Scope.PRIVATE)
dismiss()
}
findViewById<LinearLayout>(R.id.scope_local)?.setOnClickListener {
userInfoAdapter.updateScope(position, Scope.LOCAL)
dismiss()
}
findViewById<LinearLayout>(R.id.scope_federated)?.setOnClickListener {
userInfoAdapter.updateScope(position, Scope.FEDERATED)
dismiss()
}
findViewById<LinearLayout>(R.id.scope_published)?.setOnClickListener {
userInfoAdapter.updateScope(position, Scope.PUBLISHED)
dismiss()
}
}
}

View file

@ -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";
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="m9,1c-1.746,0 -3,1.43 -3,2.8 0,1.4 0.1,2.4 0.8,3.5 0.224,0.29 0.485,0.35 0.7,0.6 0.135,0.5 0.24,1 0.1,1.5 -0.436,0.153 -0.85,0.332 -1.27,0.5 -0.51,-0.273 -1.1,-0.5 -1.61,-0.7 -0.07,-0.28 -0.02,-0.487 0.05,-0.75 0.12,-0.125 0.23,-0.18 0.36,-0.3 0.37,-0.45 0.39,-1.21 0.39,-1.75 0,-0.8 -0.72,-1.4 -1.5,-1.4 -0.87,0 -1.5,0.72 -1.5,1.4h-0.02c0,0.7 0.05,1.2 0.4,1.75 0.1,0.15 0.242,0.175 0.35,0.3 0.0674,0.25 0.121,0.5 0.05,0.75 -0.64,0.223 -1.244,0.5 -1.8,0.8 -0.42,0.3 -0.233,0.182 -0.5,1.15 -0.124,0.5 1.3,0.73 2.32,0.81 -0.05,0.275 -0.12,0.64 -0.32,1.34 -0.32,1.25 4.353,1.7 6,1.7 2.43,0 6.313,-0.456 5.98,-1.7 -0.52,-1.94 -0.208,-1.71 -0.98,-2.3 -1.09,-0.654 -2.452,-1.167 -3.6,-1.6 -0.15,-0.557 -0.04,-0.97 0.1,-1.5 0.235,-0.25 0.5,-0.36 0.72,-0.6 0.69,-0.884 0.78,-2.424 0.78,-3.5 0,-1.586 -1.43,-2.8 -3,-2.8z" />
</vector>

View file

@ -0,0 +1,23 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#757575" android:pathData="M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="m7.95,0.65c-4.1,0 -7.4,3.3 -7.4,7.4s3.3,7.4 7.4,7.4 7.4,-3.3 7.4,-7.4 -3.3,-7.4 -7.4,-7.4zM8.75,1.55c1.3,0 2.4,0.8 3.5,1.3l1.8,2.5 -0.3,1.1 0.6,0.3v2.4c-0.2,0.7 -0.6,1.3 -0.9,2 -0.2,0.1 0,-0.8 -0.1,-1 0,-0.6 -0.5,-0.6 -0.9,-0.2 -0.4,0.3 -1.4,0.3 -1.5,-0.4 -0.3,-0.8 0,-1.7 0.3,-2.5l-0.6,-0.7 0.2,-1.8 -0.8,-0.9 0.2,-1 -1,-0.6c-0.2,-0.2 -0.6,-0.2 -0.7,-0.4 0.1,0 0.2,-0.1 0.2,-0.1zM6.15,1.65s0.1,0 0.1,0.1c0.4,0.2 -0.1,0.4 -0.2,0.6 -0.5,0.3 0.3,0.7 0.5,1 0.4,-0.1 0.8,-0.7 1.4,-0.5 0.7,-0.2 0.6,0.6 1.1,1 0.1,0.2 0.9,0.8 0.4,0.6 -0.5,-0.4 -1,-0.4 -1.3,0.1 -0.8,0.5 -0.3,-0.9 -0.7,-1.2 -0.6,-0.7 -0.4,0.5 -0.4,0.9 -0.4,0 -1.1,-0.3 -1.5,0.2l0.4,0.6 0.5,-0.7c0,-0.3 0.1,0.2 0.3,0.3 0.1,0.2 0.8,0.7 0.3,0.9 -0.8,0.4 -1.4,1.1 -2.1,1.7 -0.2,0.5 -0.7,0.4 -1,0 -0.7,-0.4 -0.7,0.7 -0.6,1.1l0.6,-0.4v1.1c-0.4,0.4 -0.9,-0.7 -1.3,-0.9v-1.6c0,-0.4 -0.1,-0.9 0,-1.3 0.8,-0.9 1.7,-1.9 2.2,-3h0.8c0.6,0.2 0.3,-0.7 0.5,-0.6zM4.95,9.85c0.1,0 0.2,0 0.3,0.1 0.8,0.1 1.4,0.7 2,1.1 0.5,0.5 1.6,0.3 1.7,1.2 -0.2,0.9 -1.1,1.4 -1.8,1.7 -0.2,0.1 -0.4,0.2 -0.6,0.2 -0.7,0.2 -1,-0.6 -1.2,-1.1 -0.3,-0.7 -1.1,-1.2 -1,-2.1 0,-0.4 0.2,-1 0.6,-1.1z" />
</vector>

View file

@ -0,0 +1,5 @@
<vector android:alpha="0.5" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF969696" android:pathData="M13,13H11V7H13M12,17.3A1.3,1.3 0,0 1,10.7 16A1.3,1.3 0,0 1,12 14.7A1.3,1.3 0,0 1,13.3 16A1.3,1.3 0,0 1,12 17.3M15.73,3H8.27L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3Z"/>
</vector>

View file

@ -0,0 +1,23 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#757575" android:pathData="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="m8,1c-2.319,0 -3.967,1.8644 -4,4v2.5h-1.5v7.5h11v-7.5h-1.5v-2.5c0,-2.27 -1.8,-3.9735 -4,-4zM8,3c1.25,0 2,0.963 2,2v2.5h-4v-2.5c0,-1.174 0.747,-2 2,-2z" />
</vector>

View file

@ -0,0 +1,23 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#757575" android:pathData="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" />
</vector>

View file

@ -0,0 +1,23 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#757575" android:pathData="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
</vector>

View file

@ -0,0 +1,25 @@
<!--
Nextcloud Android client application
Copyright (C) 2020 Nextcloud.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
Icon provided by Android Material Library in Apache License 2.0
-->
<vector android:height="24dp" android:tint="#666666"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View file

@ -0,0 +1,23 @@
<!--
@author Google LLC
Copyright (C) 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#757575" android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ededed" />
<corners android:radius="15dp" />
</shape>

View file

@ -0,0 +1,25 @@
<!--
Nextcloud Android client application
Copyright (C) 2020 Nextcloud.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
Icon provided by Android Material Library in Apache License 2.0
-->
<vector android:height="24dp" android:tint="#757575"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:strokeWidth="1"
android:pathData="M 9,16 V 10 H 5 l 7,-7 7,7 h -4 v 6 H 9 m -4,4 v -2 h 14 v 2 z"/>
</vector>

View file

@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?><!-- Nextcloud Android client application
Copyright (C) 2017 Andy Scherzinger
Copyright (C) 2017 Nextcloud
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_default"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/avatarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar_image"
android:layout_width="@dimen/avatar_size_big"
android:layout_height="@dimen/avatar_size_big"
android:layout_centerHorizontal="true"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:transitionName="userAvatar.transitionTag"
app:roundAsCircle="true" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/userinfo_fullName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/avatar_image"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:ellipsize="end"
android:textColor="@color/nc_incoming_text_default"
tools:text="John Doe" />
<TextView
android:id="@+id/userinfo_baseurl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/userinfo_fullName"
android:layout_centerHorizontal="true"
android:layout_margin="4dp"
android:ellipsize="end"
android:lines="2"
android:textColor="@color/nc_incoming_text_default"
tools:text="john@nextcloud.com" />
<LinearLayout
android:id="@+id/avatar_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/userinfo_baseurl"
android:layout_centerInParent="true"
android:layout_centerHorizontal="true"
android:orientation="horizontal"
android:visibility="invisible"
tools:visibility="visible">
<ImageButton
android:id="@+id/avatar_upload"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="@dimen/standard_half_margin"
android:layout_marginRight="@dimen/standard_half_margin"
android:tint="@color/black"
android:src="@drawable/upload"
android:background="@drawable/round_corner"
android:contentDescription="@string/upload_new_avatar_from_device" />
<ImageButton
android:id="@+id/avatar_choose"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="@dimen/standard_half_margin"
android:layout_marginRight="@dimen/standard_half_margin"
android:src="@drawable/ic_mimetype_folder"
android:background="@drawable/round_corner"
android:tint="@color/colorPrimary"
android:contentDescription="@string/choose_avatar_from_cloud" />
<ImageButton
android:id="@+id/avatar_delete"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="@dimen/standard_half_margin"
android:layout_marginRight="@dimen/standard_half_margin"
android:src="@drawable/trashbin"
android:tint="@color/black"
android:background="@drawable/round_corner"
android:contentDescription="@string/delete_avatar" />
</LinearLayout>
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/userinfo_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/user_info_details_table_item"
tools:visibility="gone" />
<include
android:id="@+id/emptyList"
layout="@layout/empty_list" />
<LinearLayout
android:id="@+id/loading_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
<com.elyeproj.loaderviewlibrary.LoaderTextView
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"
app:corners="100" />
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/user_info_icon_horizontal_margin"
android:layout_marginEnd="@dimen/standard_margin" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
<com.elyeproj.loaderviewlibrary.LoaderTextView
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"
app:corners="100" />
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/user_info_icon_horizontal_margin"
android:layout_marginEnd="@dimen/standard_margin" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
<com.elyeproj.loaderviewlibrary.LoaderTextView
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"
app:corners="100" />
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/user_info_icon_horizontal_margin"
android:layout_marginEnd="@dimen/standard_margin" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
<com.elyeproj.loaderviewlibrary.LoaderTextView
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"
app:corners="100" />
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/user_info_icon_horizontal_margin"
android:layout_marginEnd="@dimen/standard_margin" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -43,6 +43,7 @@
android:animateLayoutChanges="true">
<RelativeLayout
android:id="@+id/avatarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View file

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
~
~ 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 <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<LinearLayout
android:id="@+id/scope_private"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:orientation="horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/lock_symbol"
app:srcCompat="@drawable/ic_password" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_private_title"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_private_description" />
</LinearLayout>
</LinearLayout>
<LinearLayout
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">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/lock_symbol"
app:srcCompat="@drawable/ic_password" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_local_title"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_local_description" />
</LinearLayout>
</LinearLayout>
<LinearLayout
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">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/lock_symbol"
app:srcCompat="@drawable/ic_contacts" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_federated_title"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_federated_description" />
</LinearLayout>
</LinearLayout>
<LinearLayout
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">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/lock_symbol"
app:srcCompat="@drawable/ic_link" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_published_title"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_half_padding"
android:text="@string/scope_published_description" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Android client application
Copyright (C) 2016 Andy Scherzinger
Copyright (C) 2016 Nextcloud.
Copyright (C) 2021 Tobias Kaminsky
Copyright (C) 2021 Nextcloud.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/empty_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="@dimen/standard_margin"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/standard_double_margin">
<ImageView
android:id="@+id/empty_list_icon"
android:layout_width="@dimen/empty_list_icon_layout_width"
android:layout_height="@dimen/empty_list_icon_layout_height"
android:contentDescription="@string/file_list_folder"
android:src="@drawable/ic_user"
android:visibility="gone" />
<TextView
android:id="@+id/empty_list_view_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:paddingTop="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:text="@string/file_list_loading"
android:textSize="26sp" />
<TextView
android:id="@+id/empty_list_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:gravity="center"
android:paddingTop="@dimen/standard_half_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:text=""
android:visibility="gone" />
</LinearLayout>

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Android client application
Copyright (C) 2018 Andy Scherzinger
Copyright (C) 2018 Nextcloud
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/user_info_detail_container"
android:layout_width="match_parent"
android:layout_height="@dimen/iconized_single_line_item_layout_height"
android:orientation="horizontal">
<ImageView
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:contentDescription="@string/account_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_phone" />
<EditText
android:id="@+id/user_info_edit_text"
android:layout_width="259dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/user_info_icon_horizontal_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"
app:layout_constraintTop_toTopOf="parent"
tools:text="+49 123 456 789 12"
tools:ignore="LabelFor" />
<ImageView
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:contentDescription="@string/scope_toggle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/user_info_edit_text"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_link" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Nextcloud Android Talk application
@author Tobias Kaminsky
Copyright (C) 2021 Tobias Kaminsky
Copyright (C) 2021 Nextcloud GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/edit"
android:title="@string/edit"
app:showAsAction="ifRoom" />
</menu>

View file

@ -44,4 +44,16 @@
<dimen name="large_preview_dimension">80dp</dimen>
<dimen name="corner_radius">16dp</dimen>
<dimen name="button_corner_radius">24dp</dimen>
<dimen name="standard_margin">16dp</dimen>
<dimen name="two_line_primary_text_size">16sp</dimen>
<dimen name="drawer_header_subtext">12sp</dimen>
<dimen name="iconized_single_line_item_layout_height">56dp</dimen>
<dimen name="iconized_single_line_item_icon_size">24dp</dimen>
<dimen name="user_info_icon_horizontal_margin">24dp</dimen>
<dimen name="standard_double_margin">32dp</dimen>
<dimen name="empty_list_icon_layout_width">72dp</dimen>
<dimen name="empty_list_icon_layout_height">72dp</dimen>
<dimen name="standard_padding">16dp</dimen>
<dimen name="standard_half_padding">8dp</dimen>
<dimen name="standard_half_margin">8dp</dimen>
</resources>

View file

@ -356,7 +356,36 @@
<string name="no_phone_book_integration_due_to_permissions">No phone book integration due to missing permissions</string>
<string name="nc_phone_book_integration_chat_via">Chat via %s</string>
<string name="nc_phone_book_integration_account_not_found">Account not found</string>
<string name="avatar">Avatar</string>
<string name="account_icon">Account icon</string>
<string name="userinfo_no_info_headline">No personal info set</string>
<string name="userinfo_no_info_text">Add name, picture and contact details on your profile page.</string>
<string name="userinfo_error_text">Failed to retrieve personal user information.</string>
<string name="user_info_phone">Phone number</string>
<string name="user_info_email">E-mail</string>
<string name="user_info_address">Address</string>
<string name="user_info_website">Website</string>
<string name="user_info_twitter">Twitter</string>
<string name="user_info_displayname">Full name</string>
<string name="file_list_folder">folder</string>
<string name="file_list_loading">Loading…</string>
<string name="edit">Edit</string>
<string name="save">Save</string>
<string name="upload_new_avatar_from_device">Upload new avatar from device</string>
<string name="choose_avatar_from_cloud">Choose avatar from cloud</string>
<string name="delete_avatar">Delete avatar</string>
<string name="scope_private_title">Private</string>
<string name="scope_private_description">Don\'t show via public link</string>
<string name="lock_symbol">Lock symbol</string>
<string name="scope_local_title">Local</string>
<string name="scope_local_description">Don\'t synchronize to servers</string>
<string name="scope_federated_title">Federated</string>
<string name="scope_federated_description">Only synchronize to trusted servers</string>
<string name="scope_published_title">Published</string>
<string name="scope_published_description">Synchronize to trusted serves and the global and public address book</string>
<string name="scope_toggle">Scope toggle</string>
<!-- App Bar -->
<string name="appbar_search_in">Search in %s</string>