diff --git a/packages/app/src/components/UserSelector/index.jsx b/packages/app/src/components/UserSelector/index.jsx
new file mode 100644
index 00000000..1de361ed
--- /dev/null
+++ b/packages/app/src/components/UserSelector/index.jsx
@@ -0,0 +1,231 @@
+import React from "react"
+import * as antd from "antd"
+import classnames from "classnames"
+
+import { UserPreview } from "components"
+import { Icons, createIconRender } from "components/Icons"
+
+import useRequest from "comty.js/hooks/useRequest"
+
+import SearchModel from "comty.js/models/search"
+
+import "./index.less"
+
+const ResultsTypeDecorators = {
+ "friends": {
+ icon: "MdPeople",
+ label: "Recent"
+ },
+ "users": {
+ icon: "Users",
+ label: "Users"
+ }
+}
+
+const SelectableResults = (props) => {
+ const [invitedUsers, setInvitedUsers] = React.useState(props.invitedUsers ?? [])
+
+ let { results } = props
+
+ if (!results) {
+ return
+ }
+
+ if (typeof results !== "object") {
+ return
+ }
+
+ let keys = React.useMemo(() => {
+ let _keys = Object.keys(results)
+
+ // check if all keys are valid, if not replace as "others"
+ _keys = _keys.map((type) => {
+ if (ResultsTypeDecorators[type]) {
+ return type
+ }
+
+ return "others"
+ })
+
+ return _keys
+ }, [results])
+
+ if (keys.length === 0) {
+ return
+ }
+
+ const handleOnClick = (value) => {
+ if (props.onInviteUser) {
+ props.onInviteUser(value)
+ }
+ }
+
+ React.useEffect(() => {
+ if (!props.invitedUsers || !Array.isArray(props.invitedUsers)) {
+ return
+ }
+
+ setInvitedUsers(props.invitedUsers)
+ }, [props.invitedUsers])
+
+ return <>
+ {
+ keys.map((type, index) => {
+ const result = results[type]
+
+ if (!result || result.length === 0) {
+ return null
+ }
+
+ return
+
+ {
+ createIconRender(ResultsTypeDecorators[type].icon)
+ }
+ {ResultsTypeDecorators[type].label ?? "Unknown"}
+
+
+
+ {
+ result.map((item, index) => {
+ const invited = invitedUsers.find((user) => user._id === item._id)
+
+ return
+
{ }} />
+
+ handleOnClick(item)}
+ disabled={invited}
+ >
+ {
+ invited ? "Invited" : "Invite"
+ }
+
+
+ })
+ }
+
+
+ })
+ }
+ >
+}
+
+const UserSelector = (props) => {
+ const [loading, setLoading] = React.useState(false)
+
+ const [invitedUsers, setInvitedUsers] = React.useState([])
+
+ const [searchResult, setSearchResult] = React.useState(null)
+ const [searchValue, setSearchValue] = React.useState("")
+
+ const [L_QuickSearch, R_QuickSearch, E_QuickSearch, M_QuickSearch] = useRequest(SearchModel.quickSearch,)
+
+ const makeSearch = async (value) => {
+ if (value === "") {
+ return setSearchResult(null)
+ }
+
+ // make search request
+ const result = await SearchModel.search(value)
+
+ return setSearchResult(result)
+ }
+
+ const handleOnSearch = (e) => {
+ // not allow to input space as first character
+ if (e.target.value[0] === " ") {
+ return
+ }
+
+ setSearchValue(e.target.value)
+ }
+
+ const handleUserInvite = (user) => {
+ setInvitedUsers((users) => {
+ return [...users, user]
+ })
+
+ app.cores.sync.music.inviteToUser(user._id)
+
+ // create a timeout to remove user from invited list in 10 seconds
+ }
+
+ React.useEffect(() => {
+ const timer = setTimeout(async () => {
+ setLoading(true)
+
+ await makeSearch(searchValue)
+
+ setLoading(false)
+ }, 400)
+
+ return () => clearTimeout(timer)
+ }, [searchValue])
+
+ return
+
}
+ autoFocus={props.autoFocus ?? false}
+ onFocus={props.onFocus}
+ onBlur={props.onUnfocus}
+ />
+
+ {
+ searchResult &&
+ {
+ loading &&
+ }
+ {
+ !loading &&
+ }
+
+ }
+
+ {
+ !searchResult &&
+ {
+ L_QuickSearch &&
+ }
+ {
+ E_QuickSearch &&
+ }
+ {
+ !L_QuickSearch && !E_QuickSearch &&
+ }
+
+ }
+
+}
+
+export const openModal = (props) => {
+ return app.ModalController.open(() => )
+}
\ No newline at end of file
diff --git a/packages/app/src/components/UserSelector/index.less b/packages/app/src/components/UserSelector/index.less
new file mode 100644
index 00000000..bdb36465
--- /dev/null
+++ b/packages/app/src/components/UserSelector/index.less
@@ -0,0 +1,79 @@
+.user-selector {
+ display: flex;
+ flex-direction: column;
+
+ gap: 20px;
+
+ .user-selector_actions {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ width: 100%;
+
+ gap: 10px;
+ }
+
+ .user-selector_results {
+ display: flex;
+ flex-direction: column;
+
+ gap: 20px;
+
+ .user-selector_group {
+ gap: 10px;
+
+ background-color: var(--background-color-primary);
+ border-radius: 12px;
+
+ padding: 10px;
+
+ .user-selector_group_header {
+ font-size: 0.9rem;
+ }
+
+ .user-selector_group_results {
+ display: flex;
+ flex-direction: column;
+
+ gap: 5px;
+
+ .user-selector_result_item {
+ display: flex;
+ flex-direction: row;
+
+ justify-content: space-between;
+ align-items: center;
+
+ padding: 6px;
+
+ border-radius: 12px;
+
+ transition: all 0.2s ease-in-out;
+
+ &.selected {
+ background-color: var(--background-color-accent);
+ }
+
+ &.hidden {
+ opacity: 0;
+ height: 0px;
+ }
+
+ .userPreview {
+ width: 100%;
+
+ .info {
+ width: 90%;
+ }
+
+ .avatar {
+ transform: none;
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file