@poolse/react-native 2.1.6 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4941,9 +4941,291 @@ function useAppStateLifecycle() {
4941
4941
  }, [poolse]);
4942
4942
  }
4943
4943
 
4944
+ // src/voice/webrtc-native.ts
4945
+ function isNativeWebRtcAvailable(mod) {
4946
+ return typeof mod?.RTCPeerConnection === "function" && mod?.mediaDevices != null;
4947
+ }
4948
+ function createNativeWebRtcAdapter(mod) {
4949
+ if (!isNativeWebRtcAvailable(mod)) {
4950
+ throw new Error(
4951
+ "react-native-webrtc is missing or not linked. Install it and rebuild:\n npx expo install react-native-webrtc\nIt contains native code, so it needs a development build \u2014 it will not run in Expo Go."
4952
+ );
4953
+ }
4954
+ const rtc = mod;
4955
+ return {
4956
+ createPeerConnection(iceServers) {
4957
+ return new rtc.RTCPeerConnection({ iceServers });
4958
+ },
4959
+ getUserMedia() {
4960
+ return rtc.mediaDevices.getUserMedia({
4961
+ audio: { echoCancellation: true, noiseSuppression: true },
4962
+ video: false
4963
+ });
4964
+ },
4965
+ playRemoteStream() {
4966
+ return () => {
4967
+ };
4968
+ }
4969
+ };
4970
+ }
4971
+ function VoiceRoomBar({
4972
+ conversationId,
4973
+ labelFor,
4974
+ unavailableSlot,
4975
+ webrtc
4976
+ }) {
4977
+ const theme = usePoolseTheme();
4978
+ const available = isNativeWebRtcAvailable(webrtc);
4979
+ const opts = react.useMemo(
4980
+ () => available && webrtc ? { webrtc: createNativeWebRtcAdapter(webrtc) } : {},
4981
+ [available, webrtc]
4982
+ );
4983
+ const { status, participants, muted, join, leave, toggleMute, error } = react$1.useVoiceRoom(
4984
+ available ? conversationId : null,
4985
+ opts
4986
+ );
4987
+ const label = labelFor ?? ((id) => `User ${id.slice(0, 6)}`);
4988
+ const connected = status === "connected";
4989
+ if (!available) {
4990
+ return /* @__PURE__ */ jsxRuntime.jsx(
4991
+ reactNative.View,
4992
+ {
4993
+ style: [
4994
+ styles23.bar,
4995
+ { backgroundColor: theme.colors.surface, borderBottomColor: theme.colors.border }
4996
+ ],
4997
+ children: unavailableSlot ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles23.hint, { color: theme.colors.ink3 }], children: "Voice needs react-native-webrtc and a development build." })
4998
+ }
4999
+ );
5000
+ }
5001
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5002
+ reactNative.View,
5003
+ {
5004
+ style: [
5005
+ styles23.bar,
5006
+ { backgroundColor: theme.colors.surface, borderBottomColor: theme.colors.border }
5007
+ ],
5008
+ children: [
5009
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles23.row, children: [
5010
+ /* @__PURE__ */ jsxRuntime.jsx(
5011
+ reactNative.View,
5012
+ {
5013
+ style: [
5014
+ styles23.dot,
5015
+ { backgroundColor: connected ? theme.colors.brand : theme.colors.ink3 }
5016
+ ]
5017
+ }
5018
+ ),
5019
+ connected ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5020
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles23.people, children: participants.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles23.hint, { color: theme.colors.ink3 }], children: "Waiting for others\u2026" }) : participants.map((p) => /* @__PURE__ */ jsxRuntime.jsx(ParticipantChip, { participant: p, label: label(p.userId) }, p.userId)) }),
5021
+ /* @__PURE__ */ jsxRuntime.jsx(
5022
+ reactNative.Pressable,
5023
+ {
5024
+ onPress: toggleMute,
5025
+ accessibilityRole: "button",
5026
+ accessibilityState: { selected: muted },
5027
+ style: [
5028
+ styles23.btn,
5029
+ {
5030
+ borderColor: muted ? theme.colors.brand : theme.colors.border,
5031
+ backgroundColor: muted ? theme.colors.brandSoft : theme.colors.paper
5032
+ }
5033
+ ],
5034
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink2 }, children: muted ? "Unmute" : "Mute" })
5035
+ }
5036
+ ),
5037
+ /* @__PURE__ */ jsxRuntime.jsx(
5038
+ reactNative.Pressable,
5039
+ {
5040
+ onPress: leave,
5041
+ accessibilityRole: "button",
5042
+ style: [styles23.btn, { borderColor: theme.colors.error }],
5043
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error }, children: "Leave" })
5044
+ }
5045
+ )
5046
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5047
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles23.hint, { color: theme.colors.ink3 }], children: status === "connecting" ? "Connecting\u2026" : "Talk to everyone here" }),
5048
+ /* @__PURE__ */ jsxRuntime.jsx(
5049
+ reactNative.Pressable,
5050
+ {
5051
+ onPress: () => void join(),
5052
+ disabled: status === "connecting",
5053
+ accessibilityRole: "button",
5054
+ style: [styles23.btn, styles23.btnPrimary, { backgroundColor: theme.colors.brand }],
5055
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.onBrand, fontWeight: "600" }, children: status === "connecting" ? "Joining\u2026" : "Join Voice" })
5056
+ }
5057
+ )
5058
+ ] })
5059
+ ] }),
5060
+ error && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles23.error, { color: theme.colors.error }], children: error.message })
5061
+ ]
5062
+ }
5063
+ );
5064
+ }
5065
+ function ParticipantChip({ participant, label }) {
5066
+ const theme = usePoolseTheme();
5067
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5068
+ reactNative.View,
5069
+ {
5070
+ style: [
5071
+ styles23.chip,
5072
+ {
5073
+ backgroundColor: theme.colors.paper,
5074
+ // The speaking ring is a border colour swap rather than an
5075
+ // extra element, so the row never reflows mid-sentence.
5076
+ borderColor: participant.speaking ? theme.colors.brand : theme.colors.border,
5077
+ opacity: participant.muted ? 0.62 : 1
5078
+ }
5079
+ ],
5080
+ children: [
5081
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: { color: theme.colors.ink2, fontSize: 13 }, numberOfLines: 1, children: [
5082
+ label,
5083
+ participant.isSelf ? " (you)" : ""
5084
+ ] }),
5085
+ participant.muted && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error, fontSize: 11, fontWeight: "700" }, children: "\u2715" })
5086
+ ]
5087
+ }
5088
+ );
5089
+ }
5090
+ var styles23 = reactNative.StyleSheet.create({
5091
+ bar: { paddingHorizontal: 14, paddingVertical: 8, borderBottomWidth: reactNative.StyleSheet.hairlineWidth },
5092
+ row: { flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" },
5093
+ dot: { width: 8, height: 8, borderRadius: 4 },
5094
+ people: { flexDirection: "row", alignItems: "center", gap: 6, flexShrink: 1, flexWrap: "wrap" },
5095
+ chip: {
5096
+ flexDirection: "row",
5097
+ alignItems: "center",
5098
+ gap: 5,
5099
+ paddingHorizontal: 10,
5100
+ paddingVertical: 4,
5101
+ borderRadius: 999,
5102
+ borderWidth: 1
5103
+ },
5104
+ btn: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, borderWidth: 1 },
5105
+ btnPrimary: { borderWidth: 0 },
5106
+ hint: { fontSize: 13, flexShrink: 1 },
5107
+ error: { fontSize: 12, marginTop: 6 }
5108
+ });
5109
+ function CallScreen({ userId, labelFor, calls: external, webrtc }) {
5110
+ const theme = usePoolseTheme();
5111
+ const own = react$1.useCalls(external ? null : userId);
5112
+ const calls = external ?? own;
5113
+ const available = isNativeWebRtcAvailable(webrtc);
5114
+ const opts = react.useMemo(
5115
+ () => available && webrtc ? { webrtc: createNativeWebRtcAdapter(webrtc) } : {},
5116
+ [available, webrtc]
5117
+ );
5118
+ const voice = react$1.useVoiceRoom(
5119
+ calls.phase === "active" && available ? calls.activeConversationId : null,
5120
+ opts
5121
+ );
5122
+ react.useEffect(() => {
5123
+ if (calls.phase === "active" && voice.status === "idle") void voice.join();
5124
+ }, [calls.phase, voice]);
5125
+ const label = labelFor ?? ((id) => `User ${id.slice(0, 6)}`);
5126
+ const visible = calls.phase !== "idle";
5127
+ const hangUp = () => {
5128
+ voice.leave();
5129
+ calls.reset();
5130
+ };
5131
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, animationType: "slide", transparent: false, onRequestClose: hangUp, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles24.screen, { backgroundColor: theme.colors.paper }], children: [
5132
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles24.identity, children: [
5133
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles24.who, { color: theme.colors.ink }], children: calls.phase === "ringing-in" && calls.incoming ? label(calls.incoming.callerUserId) : calls.outgoing ? calls.outgoing.calleeUserIds.map(label).join(", ") : "" }),
5134
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles24.state, { color: theme.colors.ink3 }], children: statusLine(calls, voice.status) })
5135
+ ] }),
5136
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles24.actions, children: calls.phase === "ringing-in" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5137
+ /* @__PURE__ */ jsxRuntime.jsx(
5138
+ CallButton,
5139
+ {
5140
+ label: "Decline",
5141
+ color: theme.colors.error,
5142
+ onPress: () => void calls.decline()
5143
+ }
5144
+ ),
5145
+ /* @__PURE__ */ jsxRuntime.jsx(
5146
+ CallButton,
5147
+ {
5148
+ label: "Accept",
5149
+ color: theme.colors.brand,
5150
+ onPress: () => void calls.accept()
5151
+ }
5152
+ )
5153
+ ] }) : calls.phase === "ringing-out" ? /* @__PURE__ */ jsxRuntime.jsx(
5154
+ CallButton,
5155
+ {
5156
+ label: "Cancel",
5157
+ color: theme.colors.error,
5158
+ onPress: () => void calls.cancel()
5159
+ }
5160
+ ) : calls.phase === "declined" || calls.phase === "busy" || calls.phase === "timed-out" ? /* @__PURE__ */ jsxRuntime.jsx(CallButton, { label: "Close", color: theme.colors.ink3, onPress: calls.reset }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5161
+ /* @__PURE__ */ jsxRuntime.jsx(
5162
+ CallButton,
5163
+ {
5164
+ label: voice.muted ? "Unmute" : "Mute",
5165
+ color: theme.colors.ink2,
5166
+ onPress: voice.toggleMute
5167
+ }
5168
+ ),
5169
+ /* @__PURE__ */ jsxRuntime.jsx(CallButton, { label: "Hang up", color: theme.colors.error, onPress: hangUp })
5170
+ ] }) }),
5171
+ !available && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles24.warn, { color: theme.colors.error }], children: "react-native-webrtc is not installed \u2014 this call has no audio." }),
5172
+ (calls.error ?? voice.error) && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles24.warn, { color: theme.colors.error }], children: (calls.error ?? voice.error)?.message })
5173
+ ] }) });
5174
+ }
5175
+ function statusLine(calls, voiceStatus) {
5176
+ switch (calls.phase) {
5177
+ case "ringing-in":
5178
+ return "Incoming call\u2026";
5179
+ case "ringing-out":
5180
+ return "Ringing\u2026";
5181
+ case "declined":
5182
+ return "Call declined";
5183
+ case "busy":
5184
+ return "Already on another call";
5185
+ case "timed-out":
5186
+ return "No answer";
5187
+ case "active":
5188
+ return voiceStatus === "connected" ? "Connected" : "Connecting\u2026";
5189
+ default:
5190
+ return "";
5191
+ }
5192
+ }
5193
+ function CallButton({
5194
+ label,
5195
+ color,
5196
+ onPress
5197
+ }) {
5198
+ return /* @__PURE__ */ jsxRuntime.jsx(
5199
+ reactNative.Pressable,
5200
+ {
5201
+ onPress,
5202
+ accessibilityRole: "button",
5203
+ style: [styles24.callBtn, { backgroundColor: color }],
5204
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles24.callBtnText, children: label })
5205
+ }
5206
+ );
5207
+ }
5208
+ var styles24 = reactNative.StyleSheet.create({
5209
+ screen: { flex: 1, justifyContent: "space-between", paddingVertical: 72, paddingHorizontal: 24 },
5210
+ identity: { alignItems: "center", gap: 8, marginTop: 48 },
5211
+ who: { fontSize: 28, fontWeight: "600", textAlign: "center" },
5212
+ state: { fontSize: 15 },
5213
+ actions: { flexDirection: "row", justifyContent: "center", gap: 16 },
5214
+ callBtn: {
5215
+ paddingHorizontal: 26,
5216
+ paddingVertical: 16,
5217
+ borderRadius: 999,
5218
+ minWidth: 128,
5219
+ alignItems: "center"
5220
+ },
5221
+ callBtnText: { color: "#ffffff", fontSize: 16, fontWeight: "600" },
5222
+ warn: { fontSize: 12, textAlign: "center", marginTop: 12 }
5223
+ });
5224
+
4944
5225
  exports.AttachmentPicker = AttachmentPicker;
4945
5226
  exports.AttachmentPreview = AttachmentPreview;
4946
5227
  exports.Avatar = Avatar;
5228
+ exports.CallScreen = CallScreen;
4947
5229
  exports.ChatHeader = ChatHeader;
4948
5230
  exports.ConversationList = ConversationList;
4949
5231
  exports.ConversationView = ConversationView;
@@ -4968,8 +5250,11 @@ exports.ThreadView = ThreadView;
4968
5250
  exports.TypingIndicator = TypingIndicator;
4969
5251
  exports.UploadQueueStrip = UploadQueueStrip;
4970
5252
  exports.UserName = UserName;
5253
+ exports.VoiceRoomBar = VoiceRoomBar;
5254
+ exports.createNativeWebRtcAdapter = createNativeWebRtcAdapter;
4971
5255
  exports.defaultDarkTheme = defaultDarkTheme;
4972
5256
  exports.defaultLightTheme = defaultLightTheme;
5257
+ exports.isNativeWebRtcAvailable = isNativeWebRtcAvailable;
4973
5258
  exports.useAppStateLifecycle = useAppStateLifecycle;
4974
5259
  exports.useDisplayName = useDisplayName;
4975
5260
  exports.usePoolseTheme = usePoolseTheme;