diff --git a/Houseclub/src/main/AndroidManifest.xml b/Houseclub/src/main/AndroidManifest.xml
index 890f7467..2773dca2 100644
--- a/Houseclub/src/main/AndroidManifest.xml
+++ b/Houseclub/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/api/methods/GetSuggestedInvites.java b/Houseclub/src/main/java/me/grishka/houseclub/api/methods/GetSuggestedInvites.java
new file mode 100644
index 00000000..0d450ac6
--- /dev/null
+++ b/Houseclub/src/main/java/me/grishka/houseclub/api/methods/GetSuggestedInvites.java
@@ -0,0 +1,33 @@
+package me.grishka.houseclub.api.methods;
+
+import java.util.List;
+
+import me.grishka.appkit.api.SimpleCallback;
+import me.grishka.houseclub.api.BaseResponse;
+import me.grishka.houseclub.api.ClubhouseAPIRequest;
+import me.grishka.houseclub.api.model.Contact;
+import me.grishka.houseclub.api.model.FullUser;
+
+public class GetSuggestedInvites extends ClubhouseAPIRequest {
+
+ public GetSuggestedInvites(List contacts){
+ super("POST", "get_suggested_invites", Response.class);
+ requestBody=new GetSuggestedInvites.Body(contacts);
+ }
+
+ private static class Body{
+ public boolean upload_contacts;
+ public List contacts;
+
+ public Body(List contacts){
+ this.upload_contacts=true;
+ this.contacts=contacts;
+ }
+ }
+
+ public static class Response{
+ public List suggested_invites;
+ public int num_invites;
+ }
+
+}
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/api/methods/SearchUsers.java b/Houseclub/src/main/java/me/grishka/houseclub/api/methods/SearchUsers.java
new file mode 100644
index 00000000..a21bf124
--- /dev/null
+++ b/Houseclub/src/main/java/me/grishka/houseclub/api/methods/SearchUsers.java
@@ -0,0 +1,30 @@
+package me.grishka.houseclub.api.methods;
+
+import java.util.List;
+
+import me.grishka.houseclub.api.ClubhouseAPIRequest;
+import me.grishka.houseclub.api.model.FullUser;
+
+public class SearchUsers extends ClubhouseAPIRequest {
+
+ public SearchUsers(String query) {
+ super("POST", "search_users", Resp.class);
+ requestBody = new Body(query);
+ }
+
+ private static class Body {
+ public boolean cofollowsOnly;
+ public boolean followingOnly;
+ public boolean followersOnly;
+ public String query;
+
+ public Body(String query) {
+ this.query = query;
+ }
+ }
+
+ public static class Resp {
+ public List users;
+ public int count;
+ }
+}
\ No newline at end of file
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/api/model/Contact.java b/Houseclub/src/main/java/me/grishka/houseclub/api/model/Contact.java
new file mode 100644
index 00000000..5482a490
--- /dev/null
+++ b/Houseclub/src/main/java/me/grishka/houseclub/api/model/Contact.java
@@ -0,0 +1,21 @@
+package me.grishka.houseclub.api.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Date;
+
+public class Contact {
+ public String name, phone_number;
+ public boolean in_app, is_invited;
+ public int num_friends;
+
+ public Contact(){
+ }
+
+ public Contact(String name, String phone_number){
+ this.name=name;
+ this.phone_number=phone_number;
+ }
+
+}
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/fragments/HomeFragment.java b/Houseclub/src/main/java/me/grishka/houseclub/fragments/HomeFragment.java
index c901025b..fbfda6c6 100644
--- a/Houseclub/src/main/java/me/grishka/houseclub/fragments/HomeFragment.java
+++ b/Houseclub/src/main/java/me/grishka/houseclub/fragments/HomeFragment.java
@@ -113,20 +113,31 @@ public boolean wantsLightStatusBar(){
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
- menu.add(0,0,0,"").setIcon(R.drawable.ic_notifications).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- menu.add(0,1,0,"").setIcon(R.drawable.ic_baseline_person_24).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ inflater.inflate(R.menu.menu_home, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
- Bundle args=new Bundle();
- args.putInt("id", Integer.parseInt(ClubhouseSession.userID));
- if(item.getItemId()==0) {
- Nav.go(getActivity(), NotificationListFragment.class, args);
- } else if(item.getItemId()==1){
+ if (item.getItemId() == R.id.homeMenuProfile) {
+ Bundle args=new Bundle();
+ args.putInt("id", Integer.parseInt(ClubhouseSession.userID));
Nav.go(getActivity(), ProfileFragment.class, args);
+ return true;
+ } else if (item.getItemId() == R.id.homeMenuSearchPeople) {
+ Bundle args = new Bundle();
+ Nav.go(getActivity(), SearchListFragment.class, args);
+ return true;
+ } else if (item.getItemId() == R.id.homeMenuNotifications) {
+ Bundle args = new Bundle();
+ args.putInt("id", Integer.parseInt(ClubhouseSession.userID));
+ Nav.go(getActivity(), NotificationListFragment.class, args);
+ return true;
+ } else if(item.getItemId() == R.id.homeMenuInvite) {
+ Bundle args = new Bundle();
+ Nav.go(getActivity(), InviteListFragment.class, args);
+ return true;
}
- return true;
+ return super.onOptionsItemSelected(item);
}
private class ChannelAdapter extends RecyclerView.Adapter implements ImageLoaderRecyclerAdapter{
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/fragments/InviteListFragment.java b/Houseclub/src/main/java/me/grishka/houseclub/fragments/InviteListFragment.java
new file mode 100644
index 00000000..6a829691
--- /dev/null
+++ b/Houseclub/src/main/java/me/grishka/houseclub/fragments/InviteListFragment.java
@@ -0,0 +1,350 @@
+package me.grishka.houseclub.fragments;
+
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.ContactsContract;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import me.grishka.appkit.api.Callback;
+import me.grishka.appkit.api.ErrorResponse;
+import me.grishka.appkit.api.SimpleCallback;
+import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
+import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
+import me.grishka.appkit.utils.BindableViewHolder;
+import me.grishka.appkit.views.UsableRecyclerView;
+import me.grishka.houseclub.R;
+import me.grishka.houseclub.api.BaseResponse;
+import me.grishka.houseclub.api.methods.GetFollowers;
+import me.grishka.houseclub.api.methods.GetSuggestedInvites;
+import me.grishka.houseclub.api.methods.InviteToApp;
+import me.grishka.houseclub.api.model.Contact;
+import me.grishka.houseclub.api.model.FullUser;
+
+import static android.Manifest.permission.READ_CONTACTS;
+
+public class InviteListFragment extends SearchListFragment {
+
+ private List contacts = null;
+ private final Map a_contacts = new HashMap<>();
+ private List r_contacts = null;
+
+ private static final int REQUEST_READ_CONTACTS = 0;
+
+ private static final int limit = 50;
+
+ public InviteListFragment() {
+ min_query_lenght = 0;
+ }
+
+ private InviteListAdapter adapter;
+
+
+
+ @Override
+ protected RecyclerView.Adapter getAdapter(){
+ if(adapter==null){
+ adapter=new InviteListAdapter();
+ }
+ return adapter;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_READ_CONTACTS) {
+ if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ readContacts();
+ }
+ }
+ }
+
+ private boolean askContactsPermission() {
+
+ if (getContext().checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS);
+ return false;
+ }
+
+ private void readContacts() {
+ if (!askContactsPermission()) {
+ return;
+ } else {
+ contacts = getContactList();
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ reqestData();
+ }
+ });
+ }
+ }
+
+
+ private void searchContacts(String query) {
+
+ if(query == null)
+ query = "";
+
+ List users = new ArrayList<>();
+
+ users.clear();
+
+ int i =0;
+ for (Contact contact : r_contacts) {
+
+ contact.name = a_contacts.get(contact.phone_number);
+
+ Pattern pattern = Pattern.compile(Pattern.quote(query), Pattern.CASE_INSENSITIVE);
+ if (query.equals("") || (
+ pattern.matcher(contact.name + contact.phone_number).find())) {
+
+ FullUser user = new FullUser();
+ user.name = contact.name;
+ user.dsplayname = contact.phone_number;
+ String in_app = contact.in_app ? getString(R.string.yes) : getString(R.string.no);
+ String is_invited = contact.is_invited ? getString(R.string.yes) : getString(R.string.no);
+ user.bio = contact.phone_number + getString(R.string.contact_separator) +
+ getString(R.string.contact_in_app, in_app) + getString(R.string.contact_separator) +
+ getString(R.string.contact_is_invited, is_invited) + getString(R.string.contact_separator) +
+ getString(R.string.contact_num_friends, contact.num_friends);
+ users.add(user);
+
+ i++;
+ if(i > limit) break;
+ }
+
+ }
+
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ data.clear();
+ onDataLoaded(users, false);
+ }
+ });
+
+
+
+ }
+
+ private List getContactList() {
+
+ List m_contacts = new ArrayList<>();
+
+ ContentResolver cr = getContext().getContentResolver();
+ Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
+ null, null, null, null);
+
+ if ((cur != null ? cur.getCount() : 0) > 0) {
+ while (cur.moveToNext()) {
+
+ String id = cur.getString(
+ cur.getColumnIndex(ContactsContract.Contacts._ID));
+
+ String name = cur.getString(cur.getColumnIndex(
+ ContactsContract.Contacts.DISPLAY_NAME));
+
+ if (cur.getInt(cur.getColumnIndex(
+ ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0) {
+ Cursor pCur = cr.query(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ null,
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
+ new String[]{id}, null);
+ while (pCur.moveToNext()) {
+ String phoneNo = pCur.getString(pCur.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.NUMBER));
+
+ m_contacts.add(new Contact(name, phoneNo));
+ a_contacts.put(phoneNo, name);
+
+ }
+ pCur.close();
+ }
+
+
+ }
+ }
+ if (cur != null) {
+ cur.close();
+ }
+
+ return m_contacts;
+
+ }
+
+
+ void reqestData() {
+
+ currentRequest=new GetSuggestedInvites(contacts)
+ .setCallback(new SimpleCallback(this){
+ @Override
+ public void onSuccess(GetSuggestedInvites.Response result){
+ currentRequest=null;
+ r_contacts = result.suggested_invites;
+
+ Toast.makeText(getContext(), getString(R.string.contact_invites, result.num_invites), Toast.LENGTH_SHORT).show();
+
+ searchContacts(searchQuery);
+ }
+ })
+ .exec();
+
+ }
+
+
+ @Override
+ protected void doLoadData(int offset, int count) {
+
+ showProgress();
+
+ if(r_contacts == null) {
+ Runnable r = () -> {
+ if (contacts == null) {
+ readContacts();
+ }
+ };
+ new Thread(r).start();
+ } else {
+ searchContacts(searchQuery);
+ }
+
+
+
+ }
+
+
+
+
+
+ private class InviteListAdapter extends RecyclerView.Adapter implements ImageLoaderRecyclerAdapter {
+
+ @NonNull
+ @Override
+ public IviteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
+ return new IviteViewHolder();
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull IviteViewHolder holder, int position){
+ holder.bind(data.get(position));
+ }
+
+ @Override
+ public int getItemCount(){
+ return data.size();
+ }
+
+ @Override
+ public int getImageCountForItem(int position){
+ return data.get(position).photoUrl!=null ? 1 : 0;
+ }
+
+ @Override
+ public String getImageURL(int position, int image){
+ return data.get(position).photoUrl;
+ }
+ }
+
+ private class IviteViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
+
+ public TextView name, bio;
+ public Button followBtn;
+ public ImageView photo;
+ private Drawable placeholder=new ColorDrawable(0xFF808080);
+
+ public IviteViewHolder(){
+ super(getActivity(), R.layout.user_list_row);
+
+ name=findViewById(R.id.name);
+ bio=findViewById(R.id.bio);
+ followBtn=findViewById(R.id.follow_btn);
+ photo=findViewById(R.id.photo);
+ }
+
+ @Override
+ public void onBind(FullUser item){
+ name.setText(item.name);
+ bio.setText(item.bio);
+ followBtn.setVisibility(View.GONE);
+ photo.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void setImage(int index, Bitmap bitmap){
+ photo.setImageBitmap(bitmap);
+ }
+
+ @Override
+ public void clearImage(int index){
+ photo.setImageDrawable(placeholder);
+ }
+
+ @Override
+ public void onClick(){
+
+ String numberOnly= item.dsplayname.replaceAll("[^0-9+]", "");
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(R.string.invite_dialog_title);
+ builder.setMessage(getString(R.string.invite_dialog_text, item.name, numberOnly));
+
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new InviteToApp("", numberOnly, "")
+ .wrapProgress(getActivity())
+
+ .setCallback(new Callback(){
+ @Override
+ public void onSuccess(BaseResponse result){
+ Toast.makeText(getContext(), R.string.invite_ok, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ Toast.makeText(getContext(), R.string.invite_err, Toast.LENGTH_SHORT).show();
+ error.showToast(getContext());
+ }
+ })
+ .exec();
+ }
+ });
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ builder.show();
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/Houseclub/src/main/java/me/grishka/houseclub/fragments/SearchListFragment.java b/Houseclub/src/main/java/me/grishka/houseclub/fragments/SearchListFragment.java
new file mode 100644
index 00000000..dbb246cb
--- /dev/null
+++ b/Houseclub/src/main/java/me/grishka/houseclub/fragments/SearchListFragment.java
@@ -0,0 +1,104 @@
+package me.grishka.houseclub.fragments;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.SearchView;
+
+import me.grishka.appkit.api.SimpleCallback;
+import me.grishka.houseclub.R;
+import me.grishka.houseclub.api.methods.SearchUsers;
+
+public class SearchListFragment extends UserListFragment {
+
+ private SearchView searchView;
+ private SearchView.OnQueryTextListener onQueryTextListener;
+
+ protected static int min_query_lenght = 2;
+ protected String searchQuery;
+ private static final long DELAY = 200;
+ private long timestamp = System.currentTimeMillis();
+
+ @Override
+ public void onAttach(Activity activity){
+ super.onAttach(activity);
+ setTitle(R.string.search_people_hint);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ View search_panel = view.inflate(getContext(), R.layout.search_panel, null);
+
+ searchView = search_panel.findViewById(R.id.searchView);
+ searchView.setQueryHint(getString(R.string.search_people_hint));
+ onQueryTextListener = new OnSearchQueryTextListener();
+
+ getToolbar().addView(search_panel);
+ }
+
+ protected void onQueryChanged(String query) {
+ long currentTimeStamp = System.currentTimeMillis();
+ if (currentTimeStamp - timestamp < DELAY) {
+ timestamp = currentTimeStamp;
+ return;
+ }
+
+ if (query == null && min_query_lenght > 0 || query.length() <= min_query_lenght) {
+ timestamp = currentTimeStamp;
+ return;
+ }
+ timestamp = currentTimeStamp;
+ searchQuery = query;
+ loadData();
+ }
+
+ private class OnSearchQueryTextListener implements SearchView.OnQueryTextListener {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ onQueryChanged(query);
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ onQueryChanged(newText);
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ searchView.setOnQueryTextListener(onQueryTextListener);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ searchView.setOnQueryTextListener(null);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count) {
+ if (currentRequest != null) {
+ currentRequest.cancel();
+ }
+
+ currentRequest = new SearchUsers(searchQuery)
+ .setCallback(new SimpleCallback(this) {
+ @Override
+ public void onSuccess(SearchUsers.Resp result) {
+ currentRequest=null;
+ data.clear();
+ onDataLoaded(result.users, false);
+ }
+ })
+ .exec();
+ }
+
+
+}
\ No newline at end of file
diff --git a/Houseclub/src/main/res/drawable/ic_baseline_person_add_24.xml b/Houseclub/src/main/res/drawable/ic_baseline_person_add_24.xml
new file mode 100644
index 00000000..6656fe91
--- /dev/null
+++ b/Houseclub/src/main/res/drawable/ic_baseline_person_add_24.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/Houseclub/src/main/res/drawable/ic_search_people.xml b/Houseclub/src/main/res/drawable/ic_search_people.xml
new file mode 100644
index 00000000..927e94a5
--- /dev/null
+++ b/Houseclub/src/main/res/drawable/ic_search_people.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Houseclub/src/main/res/layout/search_panel.xml b/Houseclub/src/main/res/layout/search_panel.xml
new file mode 100644
index 00000000..4772412a
--- /dev/null
+++ b/Houseclub/src/main/res/layout/search_panel.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Houseclub/src/main/res/menu/menu_home.xml b/Houseclub/src/main/res/menu/menu_home.xml
new file mode 100644
index 00000000..d63be5b0
--- /dev/null
+++ b/Houseclub/src/main/res/menu/menu_home.xml
@@ -0,0 +1,32 @@
+
+
\ No newline at end of file
diff --git a/Houseclub/src/main/res/values/strings.xml b/Houseclub/src/main/res/values/strings.xml
index 26398c3c..4afd438f 100644
--- a/Houseclub/src/main/res/values/strings.xml
+++ b/Houseclub/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- Houseclub
+ Houseclub
Login
Phone number
Enter your phone #
@@ -64,4 +64,21 @@
This event has already ended
Please log in again to activate your account.
OK
+
+ Find people
+ Find clubs
+ Cancel
+ No users found
+ No clubs found
+
+ Send invite
+ Invite %s (%s) to Clubhouse
+ Invite sent successfully
+ Unexpected error: %s
+
+ You have %d invites
+ In Clubhouse: %s
+ Is invited: %s
+ %d friends on Clubhouse
+ ,