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