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 +