Compare commits
16 Commits
develop
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
|
fe9176ee4c | ||
|
988ae28d42 | ||
|
bf3cd7a5d5 | ||
|
916935484e | ||
|
b6a75d1fef | ||
|
5e4df03283 | ||
|
6ba4ad5560 | ||
|
7a1ae9e350 | ||
|
c0ed6bc8cd | ||
|
e992862e40 | ||
|
df89f8aea6 | ||
|
e1e9d645c4 | ||
|
82774b9a08 | ||
|
7d5e5eb75b | ||
|
392953f08e | ||
|
60c15006f2 |
17
.github/workflows/automated-tests.yml
vendored
17
.github/workflows/automated-tests.yml
vendored
@ -255,6 +255,7 @@ jobs:
|
||||
run: |
|
||||
sudo sh -c '
|
||||
apt --purge -y remove apache2-bin
|
||||
apt-mark hold firefox #hold ff once bbb-install frequently stuck at `Installing the firefox snap`
|
||||
'
|
||||
- name: Install BBB
|
||||
env:
|
||||
@ -293,16 +294,22 @@ jobs:
|
||||
echo "Retrying installation within $RETRY_INTERVAL seconds..."
|
||||
sleep $RETRY_INTERVAL
|
||||
fi
|
||||
|
||||
echo "Check if there is some process still locking:1"
|
||||
ps aux | grep -E 'dpkg|apt'
|
||||
|
||||
echo "Stop any ongoing processes related to apt-get or dpkg that might be stuck"
|
||||
# Use -q to suppress "no process found" messages
|
||||
killall -q apt-get || true
|
||||
killall -q dpkg || true
|
||||
# Kill any apt-get or dpkg processes that might be hanging
|
||||
killall -9 -q apt-get || true
|
||||
killall -9 -q dpkg || true
|
||||
|
||||
echo "Remove the lock files that may have been left behind"
|
||||
# Group lock file removal for better readability
|
||||
rm -f /var/lib/dpkg/lock-frontend
|
||||
rm -f /var/lib/dpkg/lock
|
||||
rm -f /var/cache/apt/archives/lock
|
||||
rm -f /var/cache/debconf/config.dat
|
||||
|
||||
echo "Reconfigure the package manager"
|
||||
dpkg --configure -a
|
||||
@ -311,6 +318,12 @@ jobs:
|
||||
apt-get clean
|
||||
apt-get autoremove
|
||||
|
||||
echo "Check if there is some process still locking:2"
|
||||
ps aux | grep -E 'dpkg|apt'
|
||||
|
||||
#remove all apt-update to make the install faster
|
||||
sed -i 's/apt-get update/#apt-get update/g' bbb-install.sh
|
||||
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
done
|
||||
|
||||
|
@ -4,7 +4,7 @@ import scala.collection.immutable.HashMap
|
||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
import org.bigbluebutton.core.db.{ PresAnnotationDAO, PresPageWritersDAO }
|
||||
import org.bigbluebutton.core.db.{ PresAnnotationDAO, PresAnnotationHistoryDAO, PresPageWritersDAO }
|
||||
|
||||
class WhiteboardModel extends SystemConfiguration {
|
||||
private var _whiteboards = new HashMap[String, Whiteboard]()
|
||||
@ -85,7 +85,9 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
PresAnnotationDAO.insertOrUpdateMap(meetingId, annotationsAdded)
|
||||
val annotationUpdatedAt = System.currentTimeMillis()
|
||||
PresAnnotationHistoryDAO.insertOrUpdateMap(meetingId, annotationsDiffAdded, annotationUpdatedAt)
|
||||
PresAnnotationDAO.insertOrUpdateMap(meetingId, annotationsAdded, annotationUpdatedAt)
|
||||
|
||||
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(newWb)
|
||||
@ -154,7 +156,9 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
val updatedWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(updatedWb)
|
||||
|
||||
PresAnnotationDAO.delete(meetingId, userId, annotationsIdsRemoved)
|
||||
val annotationUpdatedAt = System.currentTimeMillis()
|
||||
PresAnnotationHistoryDAO.deleteAnnotations(meetingId, wb.id, userId, annotationsIdsRemoved, annotationUpdatedAt)
|
||||
PresAnnotationDAO.deleteAnnotations(meetingId, userId, annotationsIdsRemoved, annotationUpdatedAt)
|
||||
|
||||
annotationsIdsRemoved
|
||||
}
|
||||
|
@ -20,8 +20,13 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = {
|
||||
log.debug("RECEIVED CREATE CHAT REQ MESSAGE")
|
||||
|
||||
var privateChatDisabled: Boolean = false
|
||||
var chatLocked: Boolean = false
|
||||
|
||||
if (msg.body.access == GroupChatAccess.PRIVATE) {
|
||||
privateChatDisabled = liveMeeting.props.meetingProp.disabledFeatures.contains("privateChat")
|
||||
}
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
|
||||
} yield {
|
||||
@ -45,7 +50,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
// Check if this message was sent while the lock settings was being changed.
|
||||
val isDelayedMessage = System.currentTimeMillis() - MeetingStatus2x.getPermissionsChangedOn(liveMeeting.status) < 5000
|
||||
|
||||
if (applyPermissionCheck && chatLocked && !isDelayedMessage) {
|
||||
if (privateChatDisabled ||
|
||||
(
|
||||
applyPermissionCheck &&
|
||||
chatLocked &&
|
||||
!isDelayedMessage
|
||||
)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to create a new group chat."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
|
@ -25,6 +25,7 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
|
||||
}
|
||||
|
||||
val chatDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("chat")
|
||||
var privateChatDisabled: Boolean = false
|
||||
val replyChatMessageDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("replyChatMessage")
|
||||
var chatLocked: Boolean = false
|
||||
var chatLockedForUser: Boolean = false
|
||||
@ -33,6 +34,10 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
|
||||
groupChat <- state.groupChats.find(msg.body.chatId)
|
||||
} yield {
|
||||
if (groupChat.access == GroupChatAccess.PRIVATE) {
|
||||
privateChatDisabled = liveMeeting.props.meetingProp.disabledFeatures.contains("privateChat")
|
||||
}
|
||||
|
||||
if (groupChat.access == GroupChatAccess.PUBLIC && user.userLockSettings.disablePublicChat && user.role != Roles.MODERATOR_ROLE) {
|
||||
chatLockedForUser = true
|
||||
}
|
||||
@ -54,7 +59,10 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatDisabled && !(applyPermissionCheck && chatLocked) && !chatLockedForUser) {
|
||||
if (!chatDisabled &&
|
||||
!privateChatDisabled &&
|
||||
!(applyPermissionCheck && chatLocked) &&
|
||||
!chatLockedForUser) {
|
||||
val newState = for {
|
||||
sender <- GroupChatApp.findGroupChatUser(msg.header.userId, liveMeeting.users2x)
|
||||
chat <- state.groupChats.find(msg.body.chatId)
|
||||
|
@ -4,13 +4,12 @@ import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
case class PresAnnotationDbModel(
|
||||
annotationId: String,
|
||||
pageId: String,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
annotationInfo: String,
|
||||
lastHistorySequence: Int,
|
||||
lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
annotationId: String,
|
||||
pageId: String,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
annotationInfo: String,
|
||||
lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
|
||||
class PresAnnotationDbTableDef(tag: Tag) extends Table[PresAnnotationDbModel](tag, None, "pres_annotation") {
|
||||
@ -19,77 +18,37 @@ class PresAnnotationDbTableDef(tag: Tag) extends Table[PresAnnotationDbModel](ta
|
||||
val meetingId = column[String]("meetingId")
|
||||
val userId = column[String]("userId")
|
||||
val annotationInfo = column[String]("annotationInfo")
|
||||
val lastHistorySequence = column[Int]("lastHistorySequence")
|
||||
val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt")
|
||||
// def whiteboard = foreignKey("whiteboard_fk", whiteboardId, Whiteboards)(_.whiteboardId, onDelete = ForeignKeyAction.Cascade)
|
||||
def * = (annotationId, pageId, meetingId, userId, annotationInfo, lastHistorySequence, lastUpdatedAt) <> (PresAnnotationDbModel.tupled, PresAnnotationDbModel.unapply)
|
||||
def * = (annotationId, pageId, meetingId, userId, annotationInfo, lastUpdatedAt) <> (PresAnnotationDbModel.tupled, PresAnnotationDbModel.unapply)
|
||||
}
|
||||
|
||||
object PresAnnotationDAO {
|
||||
def insertOrUpdate(meetingId: String, annotation: AnnotationVO, annotationDiff: AnnotationVO) = {
|
||||
// //TODO do it via trigger?
|
||||
// PresAnnotationHistoryDAO.insert(meetingId, annotationDiff).onComplete {
|
||||
// case Success(sequence) => {
|
||||
// DatabaseConnection.logger.debug(s"Sequence generated to PresAnnotationHistory record: $sequence")
|
||||
//
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PresAnnotationDbTableDef].insertOrUpdate(
|
||||
PresAnnotationDbModel(
|
||||
annotationId = annotation.id,
|
||||
pageId = annotation.wbId,
|
||||
meetingId = meetingId,
|
||||
userId = annotation.userId,
|
||||
annotationInfo = JsonUtils.mapToJson(annotation.annotationInfo).compactPrint,
|
||||
lastHistorySequence = 0,
|
||||
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
def insertOrUpdateMap(meetingId: String, annotations: Array[AnnotationVO], annotationUpdatedAt: Long) = {
|
||||
for {
|
||||
annotation <- annotations
|
||||
} yield {
|
||||
DatabaseConnection.enqueue(
|
||||
sqlu"""
|
||||
WITH upsert AS (
|
||||
UPDATE pres_annotation
|
||||
SET "annotationInfo"=${JsonUtils.mapToJson(annotation.annotationInfo).compactPrint},
|
||||
"lastUpdatedAt" = ${new java.sql.Timestamp(annotationUpdatedAt)}
|
||||
WHERE "annotationId" = ${annotation.id}
|
||||
RETURNING *)
|
||||
INSERT INTO pres_annotation ("annotationId", "pageId", "meetingId", "userId", "annotationInfo", "lastUpdatedAt")
|
||||
SELECT ${annotation.id}, ${annotation.wbId}, ${meetingId}, ${annotation.userId},
|
||||
${JsonUtils.mapToJson(annotation.annotationInfo).compactPrint}, ${new java.sql.Timestamp(annotationUpdatedAt)}
|
||||
WHERE NOT EXISTS (SELECT * FROM upsert)"""
|
||||
)
|
||||
)
|
||||
|
||||
// }
|
||||
// case Failure(e) => DatabaseConnection.logger.error(s"Error inserting PresAnnotationHistory: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def prepareInsertOrUpdate(meetingId: String, annotation: AnnotationVO) = {
|
||||
TableQuery[PresAnnotationDbTableDef].insertOrUpdate(
|
||||
PresAnnotationDbModel(
|
||||
annotationId = annotation.id,
|
||||
pageId = annotation.wbId,
|
||||
meetingId = meetingId,
|
||||
userId = annotation.userId,
|
||||
annotationInfo = JsonUtils.mapToJson(annotation.annotationInfo).compactPrint,
|
||||
lastHistorySequence = 0,
|
||||
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def insertOrUpdateMap(meetingId: String, annotations: Array[AnnotationVO]) = {
|
||||
DatabaseConnection.enqueue(
|
||||
DBIO.sequence(
|
||||
annotations.map { annotation =>
|
||||
prepareInsertOrUpdate(meetingId, annotation)
|
||||
}.toVector
|
||||
).transactionally
|
||||
)
|
||||
}
|
||||
|
||||
def delete(wbId: String, meetingId: String, userId: String, annotationId: String) = {
|
||||
// PresAnnotationHistoryDAO.delete(wbId, meetingId, userId, annotationId)
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PresAnnotationDbTableDef]
|
||||
.filter(_.annotationId === annotationId)
|
||||
.map(a => (a.annotationInfo, a.lastHistorySequence, a.meetingId, a.userId, a.lastUpdatedAt))
|
||||
.update("", 0, meetingId, userId, new java.sql.Timestamp(System.currentTimeMillis()))
|
||||
)
|
||||
}
|
||||
|
||||
def delete(meetingId: String, userId: String, annotationIds: Array[String]) = {
|
||||
def deleteAnnotations(meetingId: String, userId: String, annotationIds: Array[String], annotationUpdatedAt: Long) = {
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PresAnnotationDbTableDef]
|
||||
.filter(_.annotationId inSet annotationIds)
|
||||
.map(a => (a.annotationInfo, a.lastHistorySequence, a.meetingId, a.userId, a.lastUpdatedAt))
|
||||
.update("", 0, meetingId, userId, new java.sql.Timestamp(System.currentTimeMillis()))
|
||||
.map(a => (a.annotationInfo, a.meetingId, a.userId, a.lastUpdatedAt))
|
||||
.update("", meetingId, userId, new java.sql.Timestamp(annotationUpdatedAt))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,58 +4,57 @@ import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import PostgresProfile.api._
|
||||
|
||||
case class PresAnnotationHistoryDbModel(
|
||||
sequence: Option[Int] = None,
|
||||
annotationId: String,
|
||||
pageId: String,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
annotationInfo: String
|
||||
// lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
annotationInfo: String,
|
||||
updatedAt: java.sql.Timestamp
|
||||
)
|
||||
|
||||
class PresAnnotationHistoryDbTableDef(tag: Tag) extends Table[PresAnnotationHistoryDbModel](tag, None, "pres_annotation_history") {
|
||||
val sequence = column[Option[Int]]("sequence", O.PrimaryKey, O.AutoInc)
|
||||
val annotationId = column[String]("annotationId")
|
||||
val pageId = column[String]("pageId")
|
||||
val meetingId = column[String]("meetingId")
|
||||
val userId = column[String]("userId")
|
||||
val annotationInfo = column[String]("annotationInfo")
|
||||
// val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt")
|
||||
// def whiteboard = foreignKey("whiteboard_fk", whiteboardId, Whiteboards)(_.whiteboardId, onDelete = ForeignKeyAction.Cascade)
|
||||
def * = (sequence, annotationId, pageId, meetingId, userId, annotationInfo) <> (PresAnnotationHistoryDbModel.tupled, PresAnnotationHistoryDbModel.unapply)
|
||||
val updatedAt = column[java.sql.Timestamp]("updatedAt")
|
||||
def * = (annotationId, pageId, meetingId, userId, annotationInfo, updatedAt) <> (PresAnnotationHistoryDbModel.tupled, PresAnnotationHistoryDbModel.unapply)
|
||||
}
|
||||
|
||||
object PresAnnotationHistoryDAO {
|
||||
|
||||
def insert(meetingId: String, annotationDiff: AnnotationVO) = {
|
||||
DatabaseConnection.db.run(
|
||||
//TODO not being used for now
|
||||
TableQuery[PresAnnotationHistoryDbTableDef].returning(
|
||||
TableQuery[PresAnnotationHistoryDbTableDef].map(_.sequence)
|
||||
) += PresAnnotationHistoryDbModel(
|
||||
None,
|
||||
annotationId = annotationDiff.id,
|
||||
pageId = annotationDiff.wbId,
|
||||
meetingId = meetingId,
|
||||
userId = annotationDiff.userId,
|
||||
annotationInfo = JsonUtils.mapToJson(annotationDiff.annotationInfo).compactPrint
|
||||
)
|
||||
def insertOrUpdateMap(meetingId: String, annotations: Array[AnnotationVO], annotationUpdatedAt: Long) = {
|
||||
val dbModels = annotations.map { annotation =>
|
||||
PresAnnotationHistoryDbModel(
|
||||
annotationId = annotation.id,
|
||||
pageId = annotation.wbId,
|
||||
meetingId = meetingId,
|
||||
userId = annotation.userId,
|
||||
annotationInfo = JsonUtils.mapToJson(annotation.annotationInfo).compactPrint,
|
||||
updatedAt = new java.sql.Timestamp(annotationUpdatedAt)
|
||||
)
|
||||
}
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PresAnnotationHistoryDbTableDef] ++= dbModels
|
||||
)
|
||||
}
|
||||
|
||||
def delete(wbId: String, meetingId: String, userId: String, annotationId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
//TODO not being used for now
|
||||
TableQuery[PresAnnotationHistoryDbTableDef].returning(
|
||||
TableQuery[PresAnnotationHistoryDbTableDef].map(_.sequence)
|
||||
) += PresAnnotationHistoryDbModel(
|
||||
None,
|
||||
annotationId = annotationId,
|
||||
pageId = wbId,
|
||||
meetingId = meetingId,
|
||||
userId = userId,
|
||||
annotationInfo = ""
|
||||
)
|
||||
def deleteAnnotations(meetingId: String, pageId: String, userId: String, annotations: Array[String], annotationUpdatedAt: Long) = {
|
||||
val dbModels = annotations.map { annotationId =>
|
||||
PresAnnotationHistoryDbModel(
|
||||
annotationId = annotationId,
|
||||
pageId = pageId,
|
||||
meetingId = meetingId,
|
||||
userId = userId,
|
||||
annotationInfo = "",
|
||||
updatedAt = new java.sql.Timestamp(annotationUpdatedAt)
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PresAnnotationHistoryDbTableDef] ++= dbModels
|
||||
)
|
||||
|
||||
}
|
||||
}
|
@ -1366,8 +1366,7 @@ CREATE TABLE "pres_annotation" (
|
||||
"meetingId" varchar(100),
|
||||
"userId" varchar(50),
|
||||
"annotationInfo" TEXT,
|
||||
"lastHistorySequence" integer,
|
||||
"lastUpdatedAt" timestamp with time zone DEFAULT now()
|
||||
"lastUpdatedAt" timestamp with time zone
|
||||
);
|
||||
CREATE INDEX "idx_pres_annotation_pageId" ON "pres_annotation"("pageId");
|
||||
CREATE INDEX "idx_pres_annotation_updatedAt" ON "pres_annotation"("pageId","lastUpdatedAt");
|
||||
@ -1379,25 +1378,30 @@ CREATE TABLE "pres_annotation_history" (
|
||||
"pageId" varchar(100) REFERENCES "pres_page"("pageId") ON DELETE CASCADE,
|
||||
"meetingId" varchar(100),
|
||||
"userId" varchar(50),
|
||||
"annotationInfo" TEXT
|
||||
-- "lastUpdatedAt" timestamp with time zone DEFAULT now()
|
||||
"annotationInfo" TEXT,
|
||||
"updatedAt" timestamp with time zone
|
||||
);
|
||||
CREATE INDEX "idx_pres_annotation_history_pageId" ON "pres_annotation"("pageId");
|
||||
create index "idx_pres_annotation_history_user_meeting" on "pres_annotation_history" ("userId", "meetingId");
|
||||
CREATE INDEX "idx_pres_annotation_history_updatedAt" ON "pres_annotation_history"("pageId", "updatedAt");
|
||||
|
||||
CREATE VIEW "v_pres_annotation_curr" AS
|
||||
SELECT p."meetingId", pp."presentationId", pa."annotationId", pa."pageId", pa."userId", pa."annotationInfo", pa."lastHistorySequence", pa."lastUpdatedAt"
|
||||
SELECT p."meetingId", pp."presentationId", pa."annotationId", pa."pageId", pa."userId", pa."annotationInfo",
|
||||
pa."lastUpdatedAt", "user"."isModerator" as "userIsModerator"
|
||||
FROM pres_presentation p
|
||||
JOIN pres_page pp ON pp."presentationId" = p."presentationId"
|
||||
JOIN pres_annotation pa ON pa."pageId" = pp."pageId"
|
||||
JOIN "user" on "user"."meetingId" = pa."meetingId" and "user"."userId" = pa."userId"
|
||||
WHERE p."current" IS true
|
||||
AND pp."current" IS true;
|
||||
|
||||
CREATE VIEW "v_pres_annotation_history_curr" AS
|
||||
SELECT p."meetingId", pp."presentationId", pah."pageId", pah."userId", pah."annotationId", pah."annotationInfo", pah."sequence"
|
||||
SELECT p."meetingId", pp."presentationId", pah."pageId", pah."userId", pah."annotationId", pah."annotationInfo",
|
||||
pah."updatedAt", "user"."isModerator" as "userIsModerator"
|
||||
FROM pres_presentation p
|
||||
JOIN pres_page pp ON pp."presentationId" = p."presentationId"
|
||||
JOIN pres_annotation_history pah ON pah."pageId" = pp."pageId"
|
||||
JOIN "user" on "user"."meetingId" = pah."meetingId" and "user"."userId" = pah."userId"
|
||||
WHERE p."current" IS true
|
||||
AND pp."current" IS true;
|
||||
|
||||
|
@ -24,7 +24,6 @@ select_permissions:
|
||||
- pageId
|
||||
- presentationId
|
||||
- userId
|
||||
- lastHistorySequence
|
||||
- annotationInfo
|
||||
- lastUpdatedAt
|
||||
filter:
|
||||
@ -32,9 +31,8 @@ select_permissions:
|
||||
- meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- _or:
|
||||
- user:
|
||||
isModerator:
|
||||
_eq: true
|
||||
- userIsModerator:
|
||||
_eq: true
|
||||
- meetingId:
|
||||
_eq: X-Hasura-AnnotationsNotLockedInMeeting
|
||||
- userId:
|
||||
|
@ -24,16 +24,15 @@ select_permissions:
|
||||
- pageId
|
||||
- presentationId
|
||||
- userId
|
||||
- sequence
|
||||
- updatedAt
|
||||
- annotationInfo
|
||||
filter:
|
||||
_and:
|
||||
- meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- _or:
|
||||
- user:
|
||||
isModerator:
|
||||
_eq: true
|
||||
- userIsModerator:
|
||||
_eq: true
|
||||
- meetingId:
|
||||
_eq: X-Hasura-AnnotationsNotLockedInMeeting
|
||||
- userId:
|
||||
|
@ -93,22 +93,10 @@ const ActionsBarContainer = (props) => {
|
||||
const amIPresenter = currentUserData?.presenter;
|
||||
const amIModerator = currentUserData?.isModerator;
|
||||
const [pinnedPadDataState, setPinnedPadDataState] = useState(null);
|
||||
const { data: pinnedPadData } = useDeduplicatedSubscription(
|
||||
PINNED_PAD_SUBSCRIPTION,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const { data: pinnedPadData } = await useDeduplicatedSubscription(
|
||||
PINNED_PAD_SUBSCRIPTION,
|
||||
);
|
||||
setPinnedPadDataState(pinnedPadData || []);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const isSharedNotesPinnedFromGraphql = !!pinnedPadDataState
|
||||
&& pinnedPadDataState.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
const isSharedNotesPinned = isSharedNotesPinnedFromGraphql;
|
||||
const allowExternalVideo = useIsExternalVideoEnabled();
|
||||
const connected = useReactiveVar(connectionStatus.getConnectedStatusVar());
|
||||
const intl = useIntl();
|
||||
@ -131,7 +119,12 @@ const ActionsBarContainer = (props) => {
|
||||
&& (deviceInfo.isPhone || isLayeredView.matches);
|
||||
if (actionsBarStyle.display === false) return null;
|
||||
if (!currentMeeting) return null;
|
||||
if (!pinnedPadData) return null;
|
||||
|
||||
const isSharedNotesPinnedFromGraphql = !!pinnedPadData
|
||||
&& pinnedPadData.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
const isSharedNotesPinned = isSharedNotesPinnedFromGraphql;
|
||||
return (
|
||||
<ActionsBar {
|
||||
...{
|
||||
|
@ -74,13 +74,13 @@ const intlMessages = defineMessages({
|
||||
description: 'locked element label',
|
||||
},
|
||||
hideCursorsLabel: {
|
||||
id: "app.lock-viewers.hideViewersCursor",
|
||||
id: 'app.lock-viewers.hideViewersCursor',
|
||||
description: 'label for other viewers cursor',
|
||||
},
|
||||
hideAnnotationsLabel: {
|
||||
id: "app.lock-viewers.hideAnnotationsLabel",
|
||||
id: 'app.lock-viewers.hideAnnotationsLabel',
|
||||
description: 'label for other viewers annotation',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -88,7 +88,7 @@ const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
meeting: PropTypes.object.isRequired,
|
||||
meeting: PropTypes.shape({}).isRequired,
|
||||
showToggleLabel: PropTypes.bool.isRequired,
|
||||
updateLockSettings: PropTypes.func.isRequired,
|
||||
updateWebcamsOnlyForModerator: PropTypes.func.isRequired,
|
||||
@ -106,6 +106,12 @@ class LockViewersComponent extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { closeModal } = this.props;
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
toggleLockSettings(property) {
|
||||
const { lockSettingsProps } = this.state;
|
||||
|
||||
@ -129,18 +135,14 @@ class LockViewersComponent extends Component {
|
||||
displayLockStatus(status) {
|
||||
const { intl } = this.props;
|
||||
return (
|
||||
status && <Styled.ToggleLabel>
|
||||
status && (
|
||||
<Styled.ToggleLabel>
|
||||
{intl.formatMessage(intlMessages.lockedLabel)}
|
||||
</Styled.ToggleLabel>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { closeModal } = this.props;
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
closeModal,
|
||||
@ -152,6 +154,7 @@ class LockViewersComponent extends Component {
|
||||
onRequestClose,
|
||||
priority,
|
||||
isChatEnabled,
|
||||
isPrivateChatEnabled,
|
||||
isSharedNotesEnabled,
|
||||
} = this.props;
|
||||
|
||||
@ -257,7 +260,7 @@ class LockViewersComponent extends Component {
|
||||
</Styled.Row>
|
||||
|
||||
{isChatEnabled ? (
|
||||
<Fragment>
|
||||
<>
|
||||
<Styled.Row data-test="lockPublicChatItem">
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.FormElement>
|
||||
@ -283,34 +286,35 @@ class LockViewersComponent extends Component {
|
||||
</Styled.FormElementRight>
|
||||
</Styled.Col>
|
||||
</Styled.Row>
|
||||
<Styled.Row data-test="lockPrivateChatItem">
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.FormElement>
|
||||
<Styled.Label>
|
||||
{intl.formatMessage(intlMessages.privateChatLable)}
|
||||
</Styled.Label>
|
||||
</Styled.FormElement>
|
||||
</Styled.Col>
|
||||
<Styled.Col>
|
||||
<Styled.FormElementRight>
|
||||
{this.displayLockStatus(lockSettingsProps.disablePrivateChat)}
|
||||
<Toggle
|
||||
icons={false}
|
||||
defaultChecked={lockSettingsProps.disablePrivateChat}
|
||||
onChange={() => {
|
||||
this.toggleLockSettings('disablePrivateChat');
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.privateChatLable)}
|
||||
showToggleLabel={showToggleLabel}
|
||||
invertColors={invertColors}
|
||||
data-test="lockPrivateChat"
|
||||
/>
|
||||
</Styled.FormElementRight>
|
||||
</Styled.Col>
|
||||
</Styled.Row>
|
||||
</Fragment>
|
||||
) : null
|
||||
}
|
||||
{isPrivateChatEnabled ? (
|
||||
<Styled.Row data-test="lockPrivateChatItem">
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.FormElement>
|
||||
<Styled.Label>
|
||||
{intl.formatMessage(intlMessages.privateChatLable)}
|
||||
</Styled.Label>
|
||||
</Styled.FormElement>
|
||||
</Styled.Col>
|
||||
<Styled.Col>
|
||||
<Styled.FormElementRight>
|
||||
{this.displayLockStatus(lockSettingsProps.disablePrivateChat)}
|
||||
<Toggle
|
||||
icons={false}
|
||||
defaultChecked={lockSettingsProps.disablePrivateChat}
|
||||
onChange={() => {
|
||||
this.toggleLockSettings('disablePrivateChat');
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.privateChatLable)}
|
||||
showToggleLabel={showToggleLabel}
|
||||
invertColors={invertColors}
|
||||
data-test="lockPrivateChat"
|
||||
/>
|
||||
</Styled.FormElementRight>
|
||||
</Styled.Col>
|
||||
</Styled.Row>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{isSharedNotesEnabled
|
||||
? (
|
||||
<Styled.Row data-test="lockEditSharedNotesItem">
|
||||
@ -339,8 +343,7 @@ class LockViewersComponent extends Component {
|
||||
</Styled.Col>
|
||||
</Styled.Row>
|
||||
)
|
||||
: null
|
||||
}
|
||||
: null}
|
||||
<Styled.Row data-test="lockUserListItem">
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.FormElement>
|
||||
|
@ -4,7 +4,7 @@ import LockViewersComponent from './component';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { SET_LOCK_SETTINGS_PROPS, SET_WEBCAM_ONLY_FOR_MODERATOR } from './mutations';
|
||||
import useMeeting from '../../core/hooks/useMeeting';
|
||||
import { useIsChatEnabled, useIsSharedNotesEnabled } from '../../services/features';
|
||||
import { useIsChatEnabled, useIsPrivateChatEnabled, useIsSharedNotesEnabled } from '../../services/features';
|
||||
|
||||
const LockViewersContainer = (props) => {
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
@ -47,6 +47,7 @@ const LockViewersContainer = (props) => {
|
||||
usersPolicies: m.usersPolicies,
|
||||
}));
|
||||
const isChatEnabled = useIsChatEnabled();
|
||||
const isPrivateChatEnabled = useIsPrivateChatEnabled();
|
||||
const isSharedNotesEnabled = useIsSharedNotesEnabled();
|
||||
|
||||
return amIModerator && meeting && (
|
||||
@ -57,6 +58,7 @@ const LockViewersContainer = (props) => {
|
||||
showToggleLabel={false}
|
||||
meeting={meeting}
|
||||
isChatEnabled={isChatEnabled}
|
||||
isPrivateChatEnabled={isPrivateChatEnabled}
|
||||
isSharedNotesEnabled={isSharedNotesEnabled}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { toggleMuteMicrophone } from '/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service';
|
||||
import { useIsPrivateChatEnabled } from '/imports/ui/services/features';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
|
||||
export const isVoiceOnlyUser = (userId: string) => userId.toString().startsWith('v_');
|
||||
@ -31,7 +32,7 @@ export const generateActionsPermissions = (
|
||||
const parentRoomModerator = getFromUserSettings('bbb_parent_room_moderator', false);
|
||||
const isSubjectUserGuest = subjectUser.guest;
|
||||
const hasAuthority = currentUser.isModerator || amISubjectUser;
|
||||
const allowedToChatPrivately = !amISubjectUser && !isDialInUser;
|
||||
const allowedToChatPrivately = !amISubjectUser && !isDialInUser && useIsPrivateChatEnabled();
|
||||
const allowedToMuteAudio = hasAuthority
|
||||
&& subjectUserVoice?.joined
|
||||
&& !isMuted
|
||||
|
@ -191,30 +191,13 @@ const UserNotesGraphql: React.FC<UserNotesGraphqlProps> = (props) => {
|
||||
};
|
||||
|
||||
const UserNotesContainerGraphql: React.FC<UserNotesContainerGraphqlProps> = (props) => {
|
||||
type PinnedPadData = {
|
||||
sharedNotes: Array<{
|
||||
sharedNotesExtId: string;
|
||||
}>;
|
||||
};
|
||||
const { userLocks } = props;
|
||||
const disableNotes = userLocks.userNotes;
|
||||
const [pinnedPadDataState, setPinnedPadDataState] = useState<PinnedPadData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const { data: pinnedPadData } = await useDeduplicatedSubscription(
|
||||
PINNED_PAD_SUBSCRIPTION,
|
||||
);
|
||||
setPinnedPadDataState(pinnedPadData || []);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const { data: pinnedPadData } = useDeduplicatedSubscription(
|
||||
PINNED_PAD_SUBSCRIPTION,
|
||||
);
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
|
||||
const isPinned = !!pinnedPadDataState && pinnedPadDataState.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const sidebarContent = layoutSelectInput((i: any) => i.sidebarContent);
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
@ -226,7 +209,9 @@ const UserNotesContainerGraphql: React.FC<UserNotesContainerGraphqlProps> = (pro
|
||||
const hasUnreadNotes = useHasUnreadNotes();
|
||||
const markNotesAsRead = () => setNotesLastRev(rev);
|
||||
const isEnabled = NotesService.useIsEnabled();
|
||||
if (!pinnedPadData) return null;
|
||||
|
||||
const isPinned = !!pinnedPadData && pinnedPadData?.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
return (
|
||||
<UserNotesGraphql
|
||||
disableNotes={disableNotes}
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
DefaultFillStyle,
|
||||
DefaultFontStyle,
|
||||
DefaultSizeStyle,
|
||||
DefaultHorizontalAlignStyle,
|
||||
DefaultVerticalAlignStyle,
|
||||
InstancePresenceRecordType,
|
||||
setDefaultUiAssetUrls,
|
||||
setDefaultEditorAssetUrls,
|
||||
@ -552,6 +554,9 @@ const Whiteboard = React.memo((props) => {
|
||||
setTlEditor(editor);
|
||||
setTldrawAPI(editor);
|
||||
|
||||
DefaultHorizontalAlignStyle.defaultValue = isRTL ? 'end' : 'start';
|
||||
DefaultVerticalAlignStyle.defaultValue = 'start';
|
||||
|
||||
editor?.user?.updateUserPreferences({ locale: language });
|
||||
|
||||
const colorStyles = [
|
||||
|
@ -111,7 +111,6 @@ export const CURRENT_PAGE_ANNOTATIONS_QUERY = gql`query CurrentPageAnnotationsQu
|
||||
pres_annotation_curr(order_by: { lastUpdatedAt: desc }) {
|
||||
annotationId
|
||||
annotationInfo
|
||||
lastHistorySequence
|
||||
lastUpdatedAt
|
||||
pageId
|
||||
presentationId
|
||||
|
@ -131,3 +131,7 @@ export function useIsChatMessageReactionsEnabled() {
|
||||
&& window.meetingClientSettings.public.chat.toolbar.includes('reactions')
|
||||
);
|
||||
}
|
||||
|
||||
export function useIsPrivateChatEnabled() {
|
||||
return useDisabledFeatures().indexOf('privateChat') === -1;
|
||||
}
|
||||
|
@ -479,7 +479,7 @@ endWhenNoModeratorDelayInMinutes=1
|
||||
# List of features to disable (comma-separated)
|
||||
# https://docs.bigbluebutton.org/3.0/development/api/#create
|
||||
# Available options:
|
||||
# chat, sharedNotes, polls, screenshare, externalVideos, layouts, captions, liveTranscription,
|
||||
# chat, privateChat, sharedNotes, polls, screenshare, externalVideos, layouts, captions, liveTranscription,
|
||||
# breakoutRooms, importSharedNotesFromBreakoutRooms, importPresentationWithAnnotationsFromBreakoutRooms,
|
||||
# presentation, downloadPresentationWithAnnotations, downloadPresentationOriginalFile, downloadPresentationConvertedToPdf,
|
||||
# learningDashboard, learningDashboardDownloadSessionData,
|
||||
|
@ -350,7 +350,7 @@ const createEndpointTableData = [
|
||||
"name": "disabledFeatures",
|
||||
"required": false,
|
||||
"type": "String",
|
||||
"description": (<>List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<br /><ul><li><code className="language-plaintext highlighter-rouge">breakoutRooms</code>- <b>Breakout Rooms</b> </li><li><code className="language-plaintext highlighter-rouge">captions</code>- <b>Closed Caption</b> </li><li><code className="language-plaintext highlighter-rouge">chat</code>- <b>Chat</b></li><li><code className="language-plaintext highlighter-rouge">downloadPresentationWithAnnotations</code>- <b>Annotated presentation download</b></li><li><code className="language-plaintext highlighter-rouge">snapshotOfCurrentSlide</code>- <b>Allow snapshot of the current slide</b></li><li><code className="language-plaintext highlighter-rouge">externalVideos</code>- <b>Share an external video</b> </li><li><code className="language-plaintext highlighter-rouge">importPresentationWithAnnotationsFromBreakoutRooms</code>- <b>Capture breakout presentation</b></li><li><code className="language-plaintext highlighter-rouge">importSharedNotesFromBreakoutRooms</code>- <b>Capture breakout shared notes</b></li><li><code className="language-plaintext highlighter-rouge">layouts</code>- <b>Layouts</b> (allow only default layout)</li><li><code className="language-plaintext highlighter-rouge">learningDashboard</code>- <b>Learning Analytics Dashboard</b></li><li><code className="language-plaintext highlighter-rouge">learningDashboardDownloadSessionData</code>- <b>Learning Analytics Dashboard Download Session Data (prevents the option to download)</b></li><li><code className="language-plaintext highlighter-rouge">polls</code>- <b>Polls</b> </li><li><code className="language-plaintext highlighter-rouge">screenshare</code>- <b>Screen Sharing</b></li><li><code className="language-plaintext highlighter-rouge">sharedNotes</code>- <b>Shared Notes</b></li><li><code className="language-plaintext highlighter-rouge">virtualBackgrounds</code>- <b>Virtual Backgrounds</b></li><li><code className="language-plaintext highlighter-rouge">customVirtualBackgrounds</code>- <b>Virtual Backgrounds Upload</b></li><li><code className="language-plaintext highlighter-rouge">liveTranscription</code>- <b>Live Transcription</b></li><li><code className="language-plaintext highlighter-rouge">presentation</code>- <b>Presentation</b></li><li><code className="language-plaintext highlighter-rouge">cameraAsContent</code>-<b>Enables/Disables camera as a content</b></li><li><code className="language-plaintext highlighter-rouge">timer</code>- <b>disables timer</b></li><li><code className="language-plaintext highlighter-rouge">infiniteWhiteboard</code>- <b>Infinite Whiteboard (added in BigBlueButton 3.0)</b></li></ul></>)
|
||||
"description": (<>List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<br /><ul><li><code className="language-plaintext highlighter-rouge">breakoutRooms</code>- <b>Breakout Rooms</b> </li><li><code className="language-plaintext highlighter-rouge">captions</code>- <b>Closed Caption</b> </li><li><code className="language-plaintext highlighter-rouge">chat</code>- <b>Chat</b></li><li><code className="language-plaintext highlighter-rouge">privateChat</code>- <b>Private Chat</b></li><li><code className="language-plaintext highlighter-rouge">downloadPresentationWithAnnotations</code>- <b>Annotated presentation download</b></li><li><code className="language-plaintext highlighter-rouge">snapshotOfCurrentSlide</code>- <b>Allow snapshot of the current slide</b></li><li><code className="language-plaintext highlighter-rouge">externalVideos</code>- <b>Share an external video</b> </li><li><code className="language-plaintext highlighter-rouge">importPresentationWithAnnotationsFromBreakoutRooms</code>- <b>Capture breakout presentation</b></li><li><code className="language-plaintext highlighter-rouge">importSharedNotesFromBreakoutRooms</code>- <b>Capture breakout shared notes</b></li><li><code className="language-plaintext highlighter-rouge">layouts</code>- <b>Layouts</b> (allow only default layout)</li><li><code className="language-plaintext highlighter-rouge">learningDashboard</code>- <b>Learning Analytics Dashboard</b></li><li><code className="language-plaintext highlighter-rouge">learningDashboardDownloadSessionData</code>- <b>Learning Analytics Dashboard Download Session Data (prevents the option to download)</b></li><li><code className="language-plaintext highlighter-rouge">polls</code>- <b>Polls</b> </li><li><code className="language-plaintext highlighter-rouge">screenshare</code>- <b>Screen Sharing</b></li><li><code className="language-plaintext highlighter-rouge">sharedNotes</code>- <b>Shared Notes</b></li><li><code className="language-plaintext highlighter-rouge">virtualBackgrounds</code>- <b>Virtual Backgrounds</b></li><li><code className="language-plaintext highlighter-rouge">customVirtualBackgrounds</code>- <b>Virtual Backgrounds Upload</b></li><li><code className="language-plaintext highlighter-rouge">liveTranscription</code>- <b>Live Transcription</b></li><li><code className="language-plaintext highlighter-rouge">presentation</code>- <b>Presentation</b></li><li><code className="language-plaintext highlighter-rouge">cameraAsContent</code>-<b>Enables/Disables camera as a content</b></li><li><code className="language-plaintext highlighter-rouge">timer</code>- <b>disables timer</b></li><li><code className="language-plaintext highlighter-rouge">infiniteWhiteboard</code>- <b>Infinite Whiteboard (added in BigBlueButton 3.0)</b></li></ul></>)
|
||||
},
|
||||
{
|
||||
"name": "disabledFeaturesExclude",
|
||||
|
@ -26,7 +26,7 @@ gem 'journald-logger', '~> 3.0'
|
||||
gem 'jwt', '~> 2.2'
|
||||
gem 'locale', '~> 2.1'
|
||||
gem 'loofah', '~> 2.19.1'
|
||||
gem 'nokogiri', '~> 1.13.10', '>= 1.13.10'
|
||||
gem 'nokogiri', '~> 1.16.5'
|
||||
gem 'open4', '~> 1.3'
|
||||
gem 'rb-inotify', '~> 0.10'
|
||||
gem 'redis', '~> 4.1'
|
||||
|
@ -2,7 +2,7 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
absolute_time (1.0.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.0.7.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@ -12,11 +12,11 @@ GEM
|
||||
activesupport (>= 5.0.0.1, < 8)
|
||||
rexml
|
||||
builder (3.2.4)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.3.4)
|
||||
crass (1.0.6)
|
||||
fastimage (2.2.6)
|
||||
ffi (1.15.5)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
java_properties (0.0.4)
|
||||
journald-logger (3.1.0)
|
||||
@ -28,14 +28,14 @@ GEM
|
||||
loofah (2.19.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mini_portile2 (2.8.0)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.14.4)
|
||||
mono_logger (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
nokogiri (1.13.10)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
nokogiri (1.16.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
open4 (1.3.4)
|
||||
optimist (3.0.1)
|
||||
@ -44,7 +44,7 @@ GEM
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rack (2.2.8)
|
||||
rack (2.2.8.1)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rainbow (3.1.1)
|
||||
@ -60,8 +60,7 @@ GEM
|
||||
multi_json (~> 1.0)
|
||||
redis-namespace (~> 1.6)
|
||||
sinatra (>= 0.9.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rexml (3.3.9)
|
||||
rubocop (1.34.1)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
@ -82,7 +81,6 @@ GEM
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-protection (= 3.1.0)
|
||||
tilt (~> 2.0)
|
||||
strscan (3.1.0)
|
||||
tilt (2.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@ -102,7 +100,7 @@ DEPENDENCIES
|
||||
locale (~> 2.1)
|
||||
loofah (~> 2.19.1)
|
||||
minitest (~> 5.14.1)
|
||||
nokogiri (~> 1.13.10, >= 1.13.10)
|
||||
nokogiri (~> 1.16.5)
|
||||
open4 (~> 1.3)
|
||||
optimist
|
||||
rake (>= 12.3, < 14)
|
||||
|
Loading…
Reference in New Issue
Block a user