You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

reducer.ts 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. import { AnyAction } from 'redux';
  2. import { UPDATE_CONFERENCE_METADATA } from '../conference/actionTypes';
  3. import { MEDIA_TYPE } from '../media/constants';
  4. import ReducerRegistry from '../redux/ReducerRegistry';
  5. import { set } from '../redux/functions';
  6. import {
  7. DOMINANT_SPEAKER_CHANGED,
  8. NOTIFIED_TO_SPEAK,
  9. OVERWRITE_PARTICIPANT_NAME,
  10. PARTICIPANT_ID_CHANGED,
  11. PARTICIPANT_JOINED,
  12. PARTICIPANT_LEFT,
  13. PARTICIPANT_SOURCES_UPDATED,
  14. PARTICIPANT_UPDATED,
  15. PIN_PARTICIPANT,
  16. RAISE_HAND_CLEAR,
  17. RAISE_HAND_UPDATED,
  18. SCREENSHARE_PARTICIPANT_NAME_CHANGED,
  19. SET_LOADABLE_AVATAR_URL
  20. } from './actionTypes';
  21. import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
  22. import {
  23. isLocalScreenshareParticipant,
  24. isParticipantModerator,
  25. isRemoteScreenshareParticipant,
  26. isScreenShareParticipant
  27. } from './functions';
  28. import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
  29. /**
  30. * Participant object.
  31. *
  32. * @typedef {Object} Participant
  33. * @property {string} id - Participant ID.
  34. * @property {string} name - Participant name.
  35. * @property {string} avatar - Path to participant avatar if any.
  36. * @property {string} role - Participant role.
  37. * @property {boolean} local - If true, participant is local.
  38. * @property {boolean} pinned - If true, participant is currently a
  39. * "PINNED_ENDPOINT".
  40. * @property {boolean} dominantSpeaker - If this participant is the dominant
  41. * speaker in the (associated) conference, {@code true}; otherwise,
  42. * {@code false}.
  43. * @property {string} email - Participant email.
  44. */
  45. /**
  46. * The participant properties which cannot be updated through
  47. * {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
  48. * be modified through property-dedicated actions.
  49. *
  50. * @type {string[]}
  51. */
  52. const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
  53. // The following properties identify the participant:
  54. 'conference',
  55. 'id',
  56. 'local',
  57. // The following properties can only be modified through property-dedicated
  58. // actions:
  59. 'dominantSpeaker',
  60. 'pinned'
  61. ];
  62. const DEFAULT_STATE = {
  63. dominantSpeaker: undefined,
  64. fakeParticipants: new Map(),
  65. local: undefined,
  66. localScreenShare: undefined,
  67. numberOfNonModeratorParticipants: 0,
  68. numberOfParticipantsDisabledE2EE: 0,
  69. numberOfParticipantsNotSupportingE2EE: 0,
  70. overwrittenNameList: {},
  71. pinnedParticipant: undefined,
  72. raisedHandsQueue: [],
  73. remote: new Map(),
  74. remoteVideoSources: new Set<string>(),
  75. sortedRemoteVirtualScreenshareParticipants: new Map(),
  76. sortedRemoteParticipants: new Map(),
  77. speakersList: new Map()
  78. };
  79. export interface IParticipantsState {
  80. dominantSpeaker?: string;
  81. fakeParticipants: Map<string, IParticipant>;
  82. local?: ILocalParticipant;
  83. localScreenShare?: IParticipant;
  84. numberOfNonModeratorParticipants: number;
  85. numberOfParticipantsDisabledE2EE: number;
  86. numberOfParticipantsNotSupportingE2EE: number;
  87. overwrittenNameList: { [id: string]: string; };
  88. pinnedParticipant?: string;
  89. raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
  90. remote: Map<string, IParticipant>;
  91. remoteVideoSources: Set<string>;
  92. sortedRemoteParticipants: Map<string, string>;
  93. sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
  94. speakersList: Map<string, string>;
  95. }
  96. /**
  97. * Listen for actions which add, remove, or update the set of participants in
  98. * the conference.
  99. *
  100. * @param {IParticipant[]} state - List of participants to be modified.
  101. * @param {Object} action - Action object.
  102. * @param {string} action.type - Type of action.
  103. * @param {IParticipant} action.participant - Information about participant to be
  104. * added/removed/modified.
  105. * @returns {IParticipant[]}
  106. */
  107. ReducerRegistry.register<IParticipantsState>('features/base/participants',
  108. (state = DEFAULT_STATE, action): IParticipantsState => {
  109. switch (action.type) {
  110. case NOTIFIED_TO_SPEAK: {
  111. return {
  112. ...state,
  113. raisedHandsQueue: state.raisedHandsQueue.map((item, index) => {
  114. if (index === 0) {
  115. return {
  116. ...item,
  117. hasBeenNotified: true
  118. };
  119. }
  120. return item;
  121. })
  122. };
  123. }
  124. case PARTICIPANT_ID_CHANGED: {
  125. const { local } = state;
  126. if (local) {
  127. if (action.newValue === 'local' && state.raisedHandsQueue.find(pid => pid.id === local.id)) {
  128. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== local.id);
  129. }
  130. state.local = {
  131. ...local,
  132. id: action.newValue
  133. };
  134. return {
  135. ...state
  136. };
  137. }
  138. return state;
  139. }
  140. case DOMINANT_SPEAKER_CHANGED: {
  141. const { participant } = action;
  142. const { id, previousSpeakers = [] } = participant;
  143. const { dominantSpeaker, local } = state;
  144. const newSpeakers = [ id, ...previousSpeakers ];
  145. const sortedSpeakersList: Array<Array<string>> = [];
  146. for (const speaker of newSpeakers) {
  147. if (speaker !== local?.id) {
  148. const remoteParticipant = state.remote.get(speaker);
  149. remoteParticipant
  150. && sortedSpeakersList.push(
  151. [ speaker, _getDisplayName(state, remoteParticipant?.name) ]
  152. );
  153. }
  154. }
  155. // Keep the remote speaker list sorted alphabetically.
  156. sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
  157. // Only one dominant speaker is allowed.
  158. if (dominantSpeaker) {
  159. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  160. }
  161. if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
  162. return {
  163. ...state,
  164. dominantSpeaker: id, // @ts-ignore
  165. speakersList: new Map(sortedSpeakersList)
  166. };
  167. }
  168. delete state.dominantSpeaker;
  169. return {
  170. ...state
  171. };
  172. }
  173. case PIN_PARTICIPANT: {
  174. const { participant } = action;
  175. const { id } = participant;
  176. const { pinnedParticipant } = state;
  177. // Only one pinned participant is allowed.
  178. if (pinnedParticipant) {
  179. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  180. }
  181. if (id && _updateParticipantProperty(state, id, 'pinned', true)) {
  182. return {
  183. ...state,
  184. pinnedParticipant: id
  185. };
  186. }
  187. delete state.pinnedParticipant;
  188. return {
  189. ...state
  190. };
  191. }
  192. case SET_LOADABLE_AVATAR_URL:
  193. case PARTICIPANT_UPDATED: {
  194. const { participant } = action;
  195. let { id } = participant;
  196. const { local } = participant;
  197. if (!id && local) {
  198. id = LOCAL_PARTICIPANT_DEFAULT_ID;
  199. }
  200. let newParticipant: IParticipant | null = null;
  201. const oldParticipant = local || state.local?.id === id ? state.local : state.remote.get(id);
  202. if (state.remote.has(id)) {
  203. newParticipant = _participant(oldParticipant, action);
  204. state.remote.set(id, newParticipant);
  205. } else if (id === state.local?.id) {
  206. newParticipant = state.local = _participant(state.local, action);
  207. }
  208. if (oldParticipant && newParticipant && !newParticipant.fakeParticipant) {
  209. const isModerator = isParticipantModerator(newParticipant);
  210. if (isParticipantModerator(oldParticipant) !== isModerator) {
  211. state.numberOfNonModeratorParticipants += isModerator ? -1 : 1;
  212. }
  213. const e2eeEnabled = Boolean(newParticipant.e2eeEnabled);
  214. const e2eeSupported = Boolean(newParticipant.e2eeSupported);
  215. if (Boolean(oldParticipant.e2eeEnabled) !== e2eeEnabled) {
  216. state.numberOfParticipantsDisabledE2EE += e2eeEnabled ? -1 : 1;
  217. }
  218. if (!local && Boolean(oldParticipant.e2eeSupported) !== e2eeSupported) {
  219. state.numberOfParticipantsNotSupportingE2EE += e2eeSupported ? -1 : 1;
  220. }
  221. }
  222. return {
  223. ...state
  224. };
  225. }
  226. case SCREENSHARE_PARTICIPANT_NAME_CHANGED: {
  227. const { id, name } = action;
  228. if (state.sortedRemoteVirtualScreenshareParticipants.has(id)) {
  229. state.sortedRemoteVirtualScreenshareParticipants.delete(id);
  230. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  231. sortedRemoteVirtualScreenshareParticipants.push([ id, name ]);
  232. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  233. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  234. }
  235. return { ...state };
  236. }
  237. case PARTICIPANT_JOINED: {
  238. const participant = _participantJoined(action);
  239. const {
  240. fakeParticipant,
  241. id,
  242. name,
  243. pinned,
  244. sources
  245. } = participant;
  246. const { pinnedParticipant, dominantSpeaker } = state;
  247. if (pinned) {
  248. if (pinnedParticipant) {
  249. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  250. }
  251. state.pinnedParticipant = id;
  252. }
  253. if (participant.dominantSpeaker) {
  254. if (dominantSpeaker) {
  255. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  256. }
  257. state.dominantSpeaker = id;
  258. }
  259. if (!fakeParticipant) {
  260. const isModerator = isParticipantModerator(participant);
  261. if (!isModerator) {
  262. state.numberOfNonModeratorParticipants += 1;
  263. }
  264. const { e2eeEnabled, e2eeSupported } = participant as IParticipant;
  265. if (!e2eeEnabled) {
  266. state.numberOfParticipantsDisabledE2EE += 1;
  267. }
  268. if (!participant.local && !e2eeSupported) {
  269. state.numberOfParticipantsNotSupportingE2EE += 1;
  270. }
  271. }
  272. if (participant.local) {
  273. return {
  274. ...state,
  275. local: participant
  276. };
  277. }
  278. if (isLocalScreenshareParticipant(participant)) {
  279. return {
  280. ...state,
  281. localScreenShare: participant
  282. };
  283. }
  284. state.remote.set(id, participant);
  285. if (sources?.size) {
  286. const videoSources: Map<string, ISourceInfo> | undefined = sources.get(MEDIA_TYPE.VIDEO);
  287. if (videoSources?.size) {
  288. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  289. for (const source of videoSources.keys()) {
  290. newRemoteVideoSources.add(source);
  291. }
  292. state.remoteVideoSources = newRemoteVideoSources;
  293. }
  294. }
  295. // Insert the new participant.
  296. const displayName = _getDisplayName(state, name);
  297. const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
  298. sortedRemoteParticipants.push([ id, displayName ]);
  299. sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  300. // The sort order of participants is preserved since Map remembers the original insertion order of the keys.
  301. state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
  302. if (isRemoteScreenshareParticipant(participant)) {
  303. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  304. sortedRemoteVirtualScreenshareParticipants.push([ id, name ?? '' ]);
  305. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  306. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  307. }
  308. // Exclude the screenshare participant from the fake participant count to avoid duplicates.
  309. if (fakeParticipant && !isScreenShareParticipant(participant)) {
  310. state.fakeParticipants.set(id, participant);
  311. }
  312. return { ...state };
  313. }
  314. case PARTICIPANT_LEFT: {
  315. // XXX A remote participant is uniquely identified by their id in a
  316. // specific JitsiConference instance. The local participant is uniquely
  317. // identified by the very fact that there is only one local participant
  318. // (and the fact that the local participant "joins" at the beginning of
  319. // the app and "leaves" at the end of the app).
  320. const { conference, id } = action.participant;
  321. const {
  322. fakeParticipants,
  323. sortedRemoteVirtualScreenshareParticipants,
  324. remote,
  325. local,
  326. localScreenShare,
  327. dominantSpeaker,
  328. pinnedParticipant
  329. } = state;
  330. let oldParticipant = remote.get(id);
  331. let isLocalScreenShare = false;
  332. if (oldParticipant && oldParticipant.conference === conference) {
  333. remote.delete(id);
  334. } else if (local?.id === id) {
  335. oldParticipant = state.local;
  336. delete state.local;
  337. } else if (localScreenShare?.id === id) {
  338. isLocalScreenShare = true;
  339. oldParticipant = state.local;
  340. delete state.localScreenShare;
  341. } else {
  342. // no participant found
  343. return state;
  344. }
  345. if (oldParticipant?.sources?.size) {
  346. const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
  347. if (videoSources?.size) {
  348. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  349. for (const source of videoSources.keys()) {
  350. newRemoteVideoSources.delete(source);
  351. }
  352. state.remoteVideoSources = newRemoteVideoSources;
  353. }
  354. } else if (oldParticipant?.fakeParticipant === FakeParticipant.RemoteScreenShare) {
  355. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  356. if (newRemoteVideoSources.delete(id)) {
  357. state.remoteVideoSources = newRemoteVideoSources;
  358. }
  359. }
  360. state.sortedRemoteParticipants.delete(id);
  361. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
  362. if (dominantSpeaker === id) {
  363. state.dominantSpeaker = undefined;
  364. }
  365. // Remove the participant from the list of speakers.
  366. state.speakersList.has(id) && state.speakersList.delete(id);
  367. if (pinnedParticipant === id) {
  368. state.pinnedParticipant = undefined;
  369. }
  370. if (fakeParticipants.has(id)) {
  371. fakeParticipants.delete(id);
  372. }
  373. if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
  374. sortedRemoteVirtualScreenshareParticipants.delete(id);
  375. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  376. }
  377. if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
  378. const { e2eeEnabled, e2eeSupported } = oldParticipant;
  379. if (!isParticipantModerator(oldParticipant)) {
  380. state.numberOfNonModeratorParticipants -= 1;
  381. }
  382. if (!e2eeEnabled) {
  383. state.numberOfParticipantsDisabledE2EE -= 1;
  384. }
  385. if (!oldParticipant.local && !e2eeSupported) {
  386. state.numberOfParticipantsNotSupportingE2EE -= 1;
  387. }
  388. }
  389. return { ...state };
  390. }
  391. case PARTICIPANT_SOURCES_UPDATED: {
  392. const { id, sources } = action.participant;
  393. const participant = state.remote.get(id);
  394. if (participant) {
  395. participant.sources = sources;
  396. const videoSources: Map<string, ISourceInfo> = sources.get(MEDIA_TYPE.VIDEO);
  397. if (videoSources?.size) {
  398. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  399. for (const source of videoSources.keys()) {
  400. newRemoteVideoSources.add(source);
  401. }
  402. state.remoteVideoSources = newRemoteVideoSources;
  403. }
  404. }
  405. return { ...state };
  406. }
  407. case RAISE_HAND_CLEAR: {
  408. return {
  409. ...state,
  410. raisedHandsQueue: []
  411. };
  412. }
  413. case RAISE_HAND_UPDATED: {
  414. return {
  415. ...state,
  416. raisedHandsQueue: action.queue
  417. };
  418. }
  419. case UPDATE_CONFERENCE_METADATA: {
  420. const { metadata } = action;
  421. if (metadata?.visitors?.promoted) {
  422. let participantProcessed = false;
  423. Object.entries(metadata?.visitors?.promoted).forEach(([ key, _ ]) => {
  424. const p = state.remote.get(key);
  425. if (p && !p.isPromoted) {
  426. state.remote.set(key, {
  427. ...p,
  428. isPromoted: true
  429. });
  430. participantProcessed = true;
  431. }
  432. });
  433. if (participantProcessed) {
  434. return { ...state };
  435. }
  436. }
  437. break;
  438. }
  439. case OVERWRITE_PARTICIPANT_NAME: {
  440. const { id, name } = action;
  441. return {
  442. ...state,
  443. overwrittenNameList: {
  444. ...state.overwrittenNameList,
  445. [id]: name
  446. }
  447. };
  448. }
  449. }
  450. return state;
  451. });
  452. /**
  453. * Returns the participant's display name, default string if display name is not set on the participant.
  454. *
  455. * @param {Object} state - The local participant redux state.
  456. * @param {string} name - The display name of the participant.
  457. * @returns {string}
  458. */
  459. function _getDisplayName(state: Object, name?: string): string {
  460. // @ts-ignore
  461. const config = state['features/base/config'];
  462. return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
  463. }
  464. /**
  465. * Reducer function for a single participant.
  466. *
  467. * @param {IParticipant|undefined} state - Participant to be modified.
  468. * @param {Object} action - Action object.
  469. * @param {string} action.type - Type of action.
  470. * @param {IParticipant} action.participant - Information about participant to be
  471. * added/modified.
  472. * @param {JitsiConference} action.conference - Conference instance.
  473. * @private
  474. * @returns {IParticipant}
  475. */
  476. function _participant(state: IParticipant | ILocalParticipant = { id: '' },
  477. action: AnyAction): IParticipant | ILocalParticipant {
  478. switch (action.type) {
  479. case SET_LOADABLE_AVATAR_URL:
  480. case PARTICIPANT_UPDATED: {
  481. const { participant } = action; // eslint-disable-line no-shadow
  482. const newState = { ...state };
  483. for (const key in participant) {
  484. if (participant.hasOwnProperty(key)
  485. && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
  486. === -1) {
  487. // @ts-ignore
  488. newState[key] = participant[key];
  489. }
  490. }
  491. return newState;
  492. }
  493. }
  494. return state;
  495. }
  496. /**
  497. * Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
  498. * feature base/participants.
  499. *
  500. * @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
  501. * to reduce.
  502. * @private
  503. * @returns {Object} The new participant derived from the payload of the
  504. * specified {@code action} to be added into the redux state of the feature
  505. * base/participants after the reduction of the specified
  506. * {@code action}.
  507. */
  508. function _participantJoined({ participant }: { participant: IParticipant; }) {
  509. const {
  510. avatarURL,
  511. botType,
  512. dominantSpeaker,
  513. email,
  514. fakeParticipant,
  515. isPromoted,
  516. isReplacing,
  517. loadableAvatarUrl,
  518. local,
  519. name,
  520. pinned,
  521. presence,
  522. role,
  523. sources
  524. } = participant;
  525. let { conference, id } = participant;
  526. if (local) {
  527. // conference
  528. //
  529. // XXX The local participant is not identified in association with a
  530. // JitsiConference because it is identified by the very fact that it is
  531. // the local participant.
  532. conference = undefined;
  533. // id
  534. id || (id = LOCAL_PARTICIPANT_DEFAULT_ID);
  535. }
  536. return {
  537. avatarURL,
  538. botType,
  539. conference,
  540. dominantSpeaker: dominantSpeaker || false,
  541. email,
  542. fakeParticipant,
  543. id,
  544. isPromoted,
  545. isReplacing,
  546. loadableAvatarUrl,
  547. local: local || false,
  548. name,
  549. pinned: pinned || false,
  550. presence,
  551. role: role || PARTICIPANT_ROLE.NONE,
  552. sources
  553. };
  554. }
  555. /**
  556. * Updates a specific property for a participant.
  557. *
  558. * @param {State} state - The redux state.
  559. * @param {string} id - The ID of the participant.
  560. * @param {string} property - The property to update.
  561. * @param {*} value - The new value.
  562. * @returns {boolean} - True if a participant was updated and false otherwise.
  563. */
  564. function _updateParticipantProperty(state: IParticipantsState, id: string, property: string, value: boolean) {
  565. const { remote, local, localScreenShare } = state;
  566. if (remote.has(id)) {
  567. remote.set(id, set(remote.get(id) ?? {
  568. id: '',
  569. name: ''
  570. }, property as keyof IParticipant, value));
  571. return true;
  572. } else if (local?.id === id || local?.id === 'local') {
  573. // The local participant's ID can chance from something to "local" when
  574. // not in a conference.
  575. state.local = set(local, property as keyof ILocalParticipant, value);
  576. return true;
  577. } else if (localScreenShare?.id === id) {
  578. state.localScreenShare = set(localScreenShare, property as keyof IParticipant, value);
  579. return true;
  580. }
  581. return false;
  582. }