From 13bd4fec9f0876f8c853f84df480f6ab1ee396cc Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 6 Nov 2022 21:37:23 +0100 Subject: [PATCH 001/121] refactor AddChirp and DeleteChirp --- .../graph/handlers/SocialMediaHandler.scala | 83 ++++++++++--------- .../handlers/SocialMediaHandlerSuite.scala | 2 + 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index fd17f84ab6..5d39cc6f2c 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -7,9 +7,9 @@ import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.socialMedia._ import ch.epfl.pop.model.objects._ import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} -import ch.epfl.pop.storage.DbActor import spray.json._ +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Await, Future} import scala.util.{Failure, Success} @@ -37,49 +37,56 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { private def generateSocialChannel(lao_id: Hash): Channel = Channel(Channel.ROOT_CHANNEL_PREFIX + lao_id + Channel.SOCIAL_MEDIA_CHIRPS_PREFIX) def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { - writeAndPropagate(rpcMessage) match { - case Left(_) => + val ask = + for { + _ <- dbAskWritePropagate(rpcMessage) + _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") + _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + } yield () + + Await.ready(ask, duration).value match { + case Some(Success(_)) => val channelChirp: Channel = rpcMessage.getParamsChannel - channelChirp.decodeChannelLaoId match { - case Some(lao_id) => - val broadcastChannel: Channel = generateSocialChannel(lao_id) - rpcMessage.getParamsMessage match { - case Some(params) => - // we can't get the message_id as a Base64Data, it is a Hash - val chirp_id: Hash = params.message_id - val timestamp: Timestamp = params.decodedData.get.asInstanceOf[AddChirp].timestamp - val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, timestamp) - val broadcastData: Base64Data = Base64Data.encode(notifyAddChirp.toJson.toString) - Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) - case None => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, "Server failed to extract chirp id for the broadcast", rpcMessage.id)) - } - case None => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, "Server failed to extract LAO id for the broadcast", rpcMessage.id)) - } - case error @ Right(_) => error - case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.id)) + val lao_id: Hash = channelChirp.decodeChannelLaoId.get + val broadcastChannel: Channel = generateSocialChannel(lao_id) + + val params: Message = rpcMessage.getParamsMessage.get + val chirp_id: Hash = params.message_id + val timestamp: Timestamp = params.decodedData.get.asInstanceOf[AddChirp].timestamp + val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, timestamp) + val broadcastData: Base64Data = Base64Data.encode(notifyAddChirp.toJson.toString) + + Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) + + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) + case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } } def handleDeleteChirp(rpcMessage: JsonRpcRequest): GraphMessage = { - writeAndPropagate(rpcMessage) match { - case Left(_) => + val ask = + for { + _ <- dbAskWritePropagate(rpcMessage) + _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") + _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + } yield () + + Await.ready(ask, duration).value match { + case Some(Success(_)) => val channelChirp: Channel = rpcMessage.getParamsChannel - channelChirp.decodeChannelLaoId match { - case Some(lao_id) => - val broadcastChannel: Channel = generateSocialChannel(lao_id) - rpcMessage.getParamsMessage match { - case Some(params) => - val chirp_id: Hash = params.message_id - val timestamp: Timestamp = params.decodedData.get.asInstanceOf[DeleteChirp].timestamp - val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, timestamp) - val broadcastData: Base64Data = Base64Data.encode(notifyDeleteChirp.toJson.toString) - Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) - case None => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, "Server failed to extract chirp id for the broadcast", rpcMessage.id)) - } - case None => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, "Server failed to extract LAO id for the broadcast", rpcMessage.id)) - } - case error @ Right(_) => error - case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.id)) + val lao_id: Hash = channelChirp.decodeChannelLaoId.get + val broadcastChannel: Channel = generateSocialChannel(lao_id) + + val params: Message = rpcMessage.getParamsMessage.get + val chirp_id: Hash = params.message_id + val timestamp: Timestamp = params.decodedData.get.asInstanceOf[DeleteChirp].timestamp + val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, timestamp) + val broadcastData: Base64Data = Base64Data.encode(notifyDeleteChirp.toJson.toString) + + Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) + + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) + case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } } diff --git a/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala b/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala index 21ee2c795e..59878376fb 100644 --- a/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala +++ b/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala @@ -34,6 +34,8 @@ class SocialMediaHandlerSuite extends TestKit(ActorSystem("SocialMedia-DB-System system.log.info("Responding with a Nack") sender() ! Status.Failure(DbActorNAckException(1, "error")) + case x => + system.log.info(s"Received - error $x") } }) system.actorOf(dbActorMock, "MockedDB-NACK") From fb9e0d799f60d76a18a74b52a988892c3b60d375 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 6 Nov 2022 21:46:25 +0100 Subject: [PATCH 002/121] reformat code --- .../ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala | 3 +++ .../pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 5d39cc6f2c..0907806cbe 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -17,12 +17,15 @@ object SocialMediaHandler extends MessageHandler { final lazy val handlerInstance = new SocialMediaHandler(super.dbActor) def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleAddChirp(rpcMessage) + def handleDeleteChirp(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleDeleteChirp(rpcMessage) def handleNotifyAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleNotifyAddChirp(rpcMessage) + def handleNotifyDeleteChirp(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleNotifyDeleteChirp(rpcMessage) def handleAddReaction(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleAddReaction(rpcMessage) + def handleDeleteReaction(rpcMessage: JsonRpcRequest): GraphMessage = handlerInstance.handleDeleteReaction(rpcMessage) } diff --git a/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala b/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala index 59878376fb..5747ffbd7b 100644 --- a/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala +++ b/be2-scala/src/test/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandlerSuite.scala @@ -29,7 +29,7 @@ class SocialMediaHandlerSuite extends TestKit(ActorSystem("SocialMedia-DB-System val dbActorMock = Props(new Actor() { override def receive: Receive = { // You can modify the following match case to include more args, names... - case m: DbActor.WriteAndPropagate => + case m @ DbActor.WriteAndPropagate(_, _) => system.log.info("Received {}", m) system.log.info("Responding with a Nack") From 935d099bb48572278d2a8dafa868589229967c6e Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 6 Nov 2022 21:50:12 +0100 Subject: [PATCH 003/121] removed unused function --- .../epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 0907806cbe..88501c786b 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -110,9 +110,4 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { writeAndPropagate(rpcMessage) } - private def writeAndPropagate(rpcMessage: JsonRpcRequest): GraphMessage = { - val ask: Future[GraphMessage] = dbAskWritePropagate(rpcMessage) - Await.result(ask, duration) - } - } From 910707fd72586f705837614549f076a58ddf3099 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 6 Nov 2022 22:24:10 +0100 Subject: [PATCH 004/121] debug test --- .../pubsub/graph/handlers/SocialMediaHandler.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 88501c786b..fb9b0d385e 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -7,6 +7,7 @@ import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.socialMedia._ import ch.epfl.pop.model.objects._ import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} +import ch.epfl.pop.storage.DbActor import spray.json._ import scala.concurrent.ExecutionContext.Implicits.global @@ -42,9 +43,9 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { - _ <- dbAskWritePropagate(rpcMessage) _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, rpcMessage.getParamsMessage.get) } yield () Await.ready(ask, duration).value match { @@ -69,9 +70,9 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleDeleteChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { - _ <- dbAskWritePropagate(rpcMessage) _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, rpcMessage.getParamsMessage.get) } yield () Await.ready(ask, duration).value match { @@ -103,11 +104,12 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { } def handleAddReaction(rpcMessage: JsonRpcRequest): GraphMessage = { - writeAndPropagate(rpcMessage) + val ask: Future[GraphMessage] = dbAskWritePropagate(rpcMessage) + Await.result(ask, duration) } def handleDeleteReaction(rpcMessage: JsonRpcRequest): GraphMessage = { - writeAndPropagate(rpcMessage) + val ask: Future[GraphMessage] = dbAskWritePropagate(rpcMessage) + Await.result(ask, duration) } - } From d31a313667681c6477598fe2728e408d83c728db Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 7 Nov 2022 12:06:18 +0100 Subject: [PATCH 005/121] refactor coin handler --- .../pubsub/graph/handlers/CoinHandler.scala | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala index 25db159c10..0adf6ca612 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala @@ -2,26 +2,28 @@ package ch.epfl.pop.pubsub.graph.handlers import ch.epfl.pop.model.network.JsonRpcRequest import ch.epfl.pop.model.network.method.message.Message -import ch.epfl.pop.model.network.method.message.data.ObjectType -import ch.epfl.pop.model.network.method.message.data.coin.PostTransaction +import ch.epfl.pop.model.objects.DbActorNAckException import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} +import ch.epfl.pop.storage.DbActor import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Await, Future} +import scala.util.{Failure, Success} case object CoinHandler extends MessageHandler { def handlePostTransaction(rpcMessage: JsonRpcRequest): GraphMessage = { - rpcMessage.getParamsMessage match { - case Some(message: Message) => - dbAskWritePropagate(rpcMessage) - Left(rpcMessage) + val ask = + for { + _ <- checkParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") + message: Message = rpcMessage.getParamsMessage.get + _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, message) + } yield () - case _ => Right(PipelineError( - ErrorCodes.SERVER_ERROR.id, - s"Unable to handle coin message $rpcMessage. Not a post message", - rpcMessage.id - )) - } + Await.ready(ask, duration).value match { + case Some(Success(_)) => Left(rpcMessage) + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handlePostTransaction failed : ${ex.message}", rpcMessage.getId)) + case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handlePostTransaction failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) + } } } From 4a8ea49d61879c45fa7f997de9c00d86a543f928 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Fri, 18 Nov 2022 10:36:18 +0100 Subject: [PATCH 006/121] parametersToBroadcast function --- .../graph/handlers/SocialMediaHandler.scala | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index fb9b0d385e..9ac373edc4 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -43,25 +43,16 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { - _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") - _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + _ <- extractLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") + _ <- extractParameters[AddChirp](rpcMessage, "Server failed to extract chirp id for the broadcast") _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, rpcMessage.getParamsMessage.get) } yield () Await.ready(ask, duration).value match { case Some(Success(_)) => - val channelChirp: Channel = rpcMessage.getParamsChannel - val lao_id: Hash = channelChirp.decodeChannelLaoId.get - val broadcastChannel: Channel = generateSocialChannel(lao_id) - - val params: Message = rpcMessage.getParamsMessage.get - val chirp_id: Hash = params.message_id - val timestamp: Timestamp = params.decodedData.get.asInstanceOf[AddChirp].timestamp - val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, timestamp) - val broadcastData: Base64Data = Base64Data.encode(notifyAddChirp.toJson.toString) - - Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) - + val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) + val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) + Await.result(dbBroadcast(rpcMessage, channelChirp, Base64Data.encode(notifyAddChirp.toJson.toString), broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } @@ -70,25 +61,16 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleDeleteChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { - _ <- checkLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") - _ <- checkParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") + _ <- extractLaoChannel(rpcMessage, "Server failed to extract LAO id for the broadcast") + _ <- extractParameters(rpcMessage, "Server failed to extract chirp id for the broadcast") _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, rpcMessage.getParamsMessage.get) } yield () Await.ready(ask, duration).value match { case Some(Success(_)) => - val channelChirp: Channel = rpcMessage.getParamsChannel - val lao_id: Hash = channelChirp.decodeChannelLaoId.get - val broadcastChannel: Channel = generateSocialChannel(lao_id) - - val params: Message = rpcMessage.getParamsMessage.get - val chirp_id: Hash = params.message_id - val timestamp: Timestamp = params.decodedData.get.asInstanceOf[DeleteChirp].timestamp - val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, timestamp) - val broadcastData: Base64Data = Base64Data.encode(notifyDeleteChirp.toJson.toString) - - Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastData, broadcastChannel), duration) - + val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) + val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) + Await.result(dbBroadcast(rpcMessage, channelChirp, Base64Data.encode(notifyDeleteChirp.toJson.toString), broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } @@ -112,4 +94,16 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { val ask: Future[GraphMessage] = dbAskWritePropagate(rpcMessage) Await.result(ask, duration) } + + // generates the parameters that will be used to broadcast the chirps + private def parametersToBroadcast[T](rpcMessage: JsonRpcRequest): (Hash, Channel, T, Channel) = { + val channelChirp: Channel = rpcMessage.getParamsChannel + val lao_id: Hash = channelChirp.decodeChannelLaoId.get + val broadcastChannel: Channel = generateSocialChannel(lao_id) + val params: Message = rpcMessage.getParamsMessage.get + val chirp_id: Hash = params.message_id + val data: T = params.decodedData.get.asInstanceOf[T] + + (chirp_id, channelChirp, data, broadcastChannel) + } } From 5de8271e78249cc0efade46689af596c0da17f08 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Fri, 18 Nov 2022 11:26:12 +0100 Subject: [PATCH 007/121] removed comments --- .../epfl/pop/pubsub/graph/handlers/RollCallHandler.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala index 73b9c2f2ef..3be9e215d0 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala @@ -43,12 +43,9 @@ class RollCallHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleCreateRollCall(rpcRequest: JsonRpcRequest): GraphMessage = { val ask = for { - (_, message, somedata) <- extractParameters[CreateRollCall](rpcRequest, serverUnexpectedAnswer) - data: CreateRollCall = somedata.get - // message: Message = rpcRequest.getParamsMessage.get - // data: CreateRollCall = message.decodedData.get.asInstanceOf[CreateRollCall] + (_, message, data) <- extractParameters[CreateRollCall](rpcRequest, serverUnexpectedAnswer) // we are using the rollcall id instead of the message_id at rollcall creation - rollCallChannel: Channel = Channel(s"${Channel.ROOT_CHANNEL_PREFIX}${data.id}") + rollCallChannel: Channel = Channel(s"${Channel.ROOT_CHANNEL_PREFIX}${data.get.id}") laoId: Hash = rpcRequest.extractLaoId _ <- dbActor ? DbActor.AssertChannelMissing(rollCallChannel) // we create a new channel to write uniquely the RollCall, this ensures then if the RollCall already exists or not From e56829a893c99efe60c0237bb8671ce45c40340b Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Fri, 18 Nov 2022 11:43:20 +0100 Subject: [PATCH 008/121] remove commented lines --- .../ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala index 3be9e215d0..6dc5e707b1 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/RollCallHandler.scala @@ -66,7 +66,6 @@ class RollCallHandler(dbRef: => AskableActorRef) extends MessageHandler { val ask = for { (_, message, _) <- extractParameters[OpenRollCall](rpcRequest, serverUnexpectedAnswer) // if it fails, throw an error - // message: Message = rpcRequest.getParamsMessage.get // this line is not executed if the first fails channel: Channel = rpcRequest.getParamsChannel laoId: Hash = rpcRequest.extractLaoId // check if the roll call already exists to open it @@ -86,7 +85,6 @@ class RollCallHandler(dbRef: => AskableActorRef) extends MessageHandler { def handleReopenRollCall(rpcRequest: JsonRpcRequest): GraphMessage = { val ask = for { (_, message, _) <- extractParameters[ReopenRollCall](rpcRequest, serverUnexpectedAnswer) - // message: Message = rpcRequest.getParamsMessage.get laoId: Hash = rpcRequest.extractLaoId _ <- dbAskWritePropagate(rpcRequest) _ <- dbActor ? DbActor.WriteRollCallData(laoId, message) @@ -104,7 +102,6 @@ class RollCallHandler(dbRef: => AskableActorRef) extends MessageHandler { (_, laoChannel) <- extractLaoChannel(rpcRequest, s"There is an issue with the data of the LAO") _ <- dbAskWritePropagate(rpcRequest) (_, message, _) <- extractParameters[CloseRollCall](rpcRequest, serverUnexpectedAnswer) - // message: Message = rpcRequest.getParamsMessage.get _ <- dbActor ? DbActor.WriteLaoData(rpcRequest.getParamsChannel, message, None) _ <- dbActor ? DbActor.WriteRollCallData(laoChannel.get, message) } yield () From 84fde3a33807a107ec9db679585c5e261e82f242 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 27 Nov 2022 22:32:35 +0100 Subject: [PATCH 009/121] create meeting refactoring and coin handler refactoring --- .../pubsub/graph/handlers/CoinHandler.scala | 20 +++++++-------- .../graph/handlers/MeetingHandler.scala | 25 +++++++++++-------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala index 0adf6ca612..fd2f16034e 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala @@ -1,29 +1,27 @@ package ch.epfl.pop.pubsub.graph.handlers import ch.epfl.pop.model.network.JsonRpcRequest -import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.objects.DbActorNAckException import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} -import ch.epfl.pop.storage.DbActor import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Await, Future} +import scala.concurrent.{Await} import scala.util.{Failure, Success} case object CoinHandler extends MessageHandler { def handlePostTransaction(rpcMessage: JsonRpcRequest): GraphMessage = { - val ask = + val ask = { for { _ <- checkParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") - message: Message = rpcMessage.getParamsMessage.get - _ <- dbActor ? DbActor.WriteAndPropagate(rpcMessage.getParamsChannel, message) + _ <- dbAskWritePropagate(rpcMessage) } yield () + } - Await.ready(ask, duration).value match { - case Some(Success(_)) => Left(rpcMessage) - case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handlePostTransaction failed : ${ex.message}", rpcMessage.getId)) - case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handlePostTransaction failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) - } + Await.ready(ask, duration).value match { + case Some(Success(_)) => Left(rpcMessage) + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handlePostTransaction failed : ${ex.message}", rpcMessage.getId)) + case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handlePostTransaction failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) + } } } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala index 855b5ebd32..d421ab0595 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala @@ -1,22 +1,27 @@ package ch.epfl.pop.pubsub.graph.handlers import ch.epfl.pop.model.network.JsonRpcRequest +import ch.epfl.pop.model.objects.DbActorNAckException import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} -import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{Await} +import scala.util.{Failure, Success} case object MeetingHandler extends MessageHandler { def handleCreateMeeting(rpcMessage: JsonRpcRequest): GraphMessage = { - rpcMessage.getParamsChannel.decodeChannelLaoId match { - case Some(_) => - val ask: Future[GraphMessage] = dbAskWritePropagate(rpcMessage) - Await.result(ask, duration) - case _ => Right(PipelineError( - ErrorCodes.INVALID_DATA.id, - s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'", - rpcMessage.id - )) + val ask = { + for { + _ <- checkParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") + _ <- dbAskWritePropagate(rpcMessage) + } yield () + } + + Await.ready(ask, duration).value match { + case Some(Success(_)) => Left(rpcMessage) + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'", rpcMessage.getId)) + case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handleCreateMeeting failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) } } From c6adaa7233b0d2abc7ec9a649a98de29b7a05dac Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Sun, 27 Nov 2022 22:38:48 +0100 Subject: [PATCH 010/121] comments correction --- .../ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala index d421ab0595..f6430c9434 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala @@ -13,14 +13,14 @@ case object MeetingHandler extends MessageHandler { def handleCreateMeeting(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = { for { - _ <- checkParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") + _ <- checkParameters(rpcMessage, s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'") _ <- dbAskWritePropagate(rpcMessage) } yield () } Await.ready(ask, duration).value match { case Some(Success(_)) => Left(rpcMessage) - case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'", rpcMessage.getId)) + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleCreateMeeting failed : ${ex.message}", rpcMessage.getId)) case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handleCreateMeeting failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) } } From 859730b39881a0fc477187bffd0a25c6f15c78d7 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Thu, 22 Dec 2022 08:00:47 +0100 Subject: [PATCH 011/121] Update patch version --- ...on+drawer+6.5.1.patch => @react-navigation+drawer+6.5.4.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fe1-web/patches/{@react-navigation+drawer+6.5.1.patch => @react-navigation+drawer+6.5.4.patch} (100%) diff --git a/fe1-web/patches/@react-navigation+drawer+6.5.1.patch b/fe1-web/patches/@react-navigation+drawer+6.5.4.patch similarity index 100% rename from fe1-web/patches/@react-navigation+drawer+6.5.1.patch rename to fe1-web/patches/@react-navigation+drawer+6.5.4.patch From 7f667e3612135267def3a84235881de68352a22c Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 26 Dec 2022 22:12:17 +0100 Subject: [PATCH 012/121] fixed merge errors --- .../scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala | 2 +- .../ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala index fd2f16034e..b7e319bcd7 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala @@ -13,7 +13,7 @@ case object CoinHandler extends MessageHandler { def handlePostTransaction(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = { for { - _ <- checkParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") + _ <- extractParameters(rpcMessage, s"Unable to handle coin message $rpcMessage. Not a post message") _ <- dbAskWritePropagate(rpcMessage) } yield () } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala index f6430c9434..394fba0cbd 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala @@ -13,7 +13,7 @@ case object MeetingHandler extends MessageHandler { def handleCreateMeeting(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = { for { - _ <- checkParameters(rpcMessage, s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'") + _ <- extractParameters(rpcMessage, s"Unable to create meeting: invalid encoded laoId '${rpcMessage.getParamsChannel}'") _ <- dbAskWritePropagate(rpcMessage) } yield () } From fa91e936271764f7bf87187fc1595b0d803114e9 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Thu, 5 Jan 2023 15:33:12 +0100 Subject: [PATCH 013/121] Make reaction buttons less attention grabbing --- fe1-web/src/core/components/PoPIconButton.tsx | 18 +++-- .../features/social/components/ChirpCard.tsx | 4 ++ .../__snapshots__/ChirpCard.test.tsx.snap | 70 +++++++++++++++---- .../SocialTopChirps.test.tsx.snap | 45 +++++++++--- 4 files changed, 109 insertions(+), 28 deletions(-) diff --git a/fe1-web/src/core/components/PoPIconButton.tsx b/fe1-web/src/core/components/PoPIconButton.tsx index bae502b0fa..ec4daab357 100644 --- a/fe1-web/src/core/components/PoPIconButton.tsx +++ b/fe1-web/src/core/components/PoPIconButton.tsx @@ -15,6 +15,18 @@ const SIZE_MAP = { const PoPIconButton = (props: IPropTypes) => { const { onPress, buttonStyle, disabled, toolbar, size, testID, name } = props; + let iconColor = Color.contrast; + + if (buttonStyle === 'secondary') { + // in case of an outlined button, the icon color + // should be the same as the border's + if (disabled) { + iconColor = Color.inactive; + } else { + iconColor = Color.accent; + } + } + return ( { disabled={disabled} toolbar={toolbar} testID={testID}> - + ); }; diff --git a/fe1-web/src/features/social/components/ChirpCard.tsx b/fe1-web/src/features/social/components/ChirpCard.tsx index 791495c278..0d389df75b 100644 --- a/fe1-web/src/features/social/components/ChirpCard.tsx +++ b/fe1-web/src/features/social/components/ChirpCard.tsx @@ -198,6 +198,7 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { onPress={() => addReaction('πŸ‘')} disabled={reactionsDisabled['πŸ‘']} size="small" + buttonStyle="secondary" toolbar /> @@ -211,6 +212,7 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { onPress={() => addReaction('πŸ‘Ž')} disabled={reactionsDisabled['πŸ‘Ž']} size="small" + buttonStyle="secondary" toolbar /> @@ -224,6 +226,7 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { onPress={() => addReaction('❀️')} disabled={reactionsDisabled['❀️']} size="small" + buttonStyle="secondary" toolbar /> @@ -237,6 +240,7 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { testID="chirp_action_options" onPress={() => showActionSheet(actionSheetOptions)} size="small" + buttonStyle="secondary" toolbar /> diff --git a/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap b/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap index b0d489da0d..e4e3719303 100644 --- a/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap +++ b/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap @@ -1199,11 +1199,14 @@ exports[`ChirpCard for deletion renders correctly for non-sender 1`] = ` "backgroundColor": "#8E8E8E", "borderColor": "#8E8E8E", }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} @@ -1288,11 +1291,14 @@ exports[`ChirpCard for deletion renders correctly for non-sender 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -1377,11 +1383,14 @@ exports[`ChirpCard for deletion renders correctly for non-sender 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} @@ -2065,11 +2074,14 @@ exports[`ChirpCard for deletion renders correctly for sender 1`] = ` "backgroundColor": "#8E8E8E", "borderColor": "#8E8E8E", }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} @@ -2154,11 +2166,14 @@ exports[`ChirpCard for deletion renders correctly for sender 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -2243,11 +2258,14 @@ exports[`ChirpCard for deletion renders correctly for sender 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} @@ -2332,11 +2350,14 @@ exports[`ChirpCard for deletion renders correctly for sender 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"ios-ellipsis-horizontal","size":16,"color":"#fff"} + {"name":"ios-ellipsis-horizontal","size":16,"color":"#3742fa"} @@ -2999,11 +3020,14 @@ exports[`ChirpCard for reaction renders correctly with reaction 1`] = ` "backgroundColor": "#8E8E8E", "borderColor": "#8E8E8E", }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} @@ -3088,11 +3112,14 @@ exports[`ChirpCard for reaction renders correctly with reaction 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -3177,11 +3204,14 @@ exports[`ChirpCard for reaction renders correctly with reaction 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} @@ -3266,11 +3296,14 @@ exports[`ChirpCard for reaction renders correctly with reaction 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"ios-ellipsis-horizontal","size":16,"color":"#fff"} + {"name":"ios-ellipsis-horizontal","size":16,"color":"#3742fa"} @@ -3933,11 +3966,14 @@ exports[`ChirpCard for reaction renders correctly without reaction 1`] = ` "backgroundColor": "#8E8E8E", "borderColor": "#8E8E8E", }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} @@ -4022,11 +4058,14 @@ exports[`ChirpCard for reaction renders correctly without reaction 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -4111,11 +4150,14 @@ exports[`ChirpCard for reaction renders correctly without reaction 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} diff --git a/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialTopChirps.test.tsx.snap b/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialTopChirps.test.tsx.snap index 69e0a3b820..44a805fa4b 100644 --- a/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialTopChirps.test.tsx.snap +++ b/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialTopChirps.test.tsx.snap @@ -635,11 +635,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#3742fa"} @@ -724,11 +727,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -813,11 +819,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} @@ -1163,11 +1172,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#3742fa"} @@ -1252,11 +1264,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -1341,11 +1356,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} @@ -1697,11 +1715,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#fff"} + {"name":"thumbs-up-sharp","size":16,"color":"#3742fa"} @@ -1786,11 +1807,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"thumbs-down-sharp","size":16,"color":"#fff"} + {"name":"thumbs-down-sharp","size":16,"color":"#3742fa"} @@ -1875,11 +1899,14 @@ exports[`SocialTopChirps renders correctly 1`] = ` "borderWidth": 1, "padding": 8, }, + Object { + "backgroundColor": "transparent", + }, ] } > - {"name":"heart","size":16,"color":"#fff"} + {"name":"heart","size":16,"color":"#3742fa"} From c11b0acf3c002b02345927e398b0d2438de278b5 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 10 Jan 2023 14:02:41 +0100 Subject: [PATCH 014/121] change makefile to disable test caching --- be1-go/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be1-go/Makefile b/be1-go/Makefile index 9be30ce009..9a11849a34 100644 --- a/be1-go/Makefile +++ b/be1-go/Makefile @@ -9,7 +9,7 @@ lint: staticcheck ./... test: protocol - go test -v -race ./... + go test -v -race ./... -count=1 test-cov: protocol go test -v -coverpkg=./... -coverprofile=coverage.out ./... -json > report.json From 046a7024d380ed5914a1212054c6493203dfd0dc Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 10 Jan 2023 14:03:54 +0100 Subject: [PATCH 015/121] remove arguments in a require.NoError --- be1-go/channel/lao/mod_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be1-go/channel/lao/mod_test.go b/be1-go/channel/lao/mod_test.go index 827e689b58..62a3bfeb34 100644 --- a/be1-go/channel/lao/mod_test.go +++ b/be1-go/channel/lao/mod_test.go @@ -554,7 +554,7 @@ func TestLAOChannel_Sends_Greeting(t *testing.T) { var laoGreet messagedata.LaoGreet err = greetMsg.UnmarshalData(&laoGreet) - require.NoError(t, err, laoGreet, catchupAnswer) + require.NoError(t, err) require.Equal(t, messagedata.LAOObject, laoGreet.Object) require.Equal(t, messagedata.LAOActionGreet, laoGreet.Action) From b40bbb7aeecfe919941ef7a3c6ce1829ff15bf96 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 10 Jan 2023 14:54:46 +0100 Subject: [PATCH 016/121] add more verifications to find wrong field in test delete chirp --- be1-go/channel/chirp/mod_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/be1-go/channel/chirp/mod_test.go b/be1-go/channel/chirp/mod_test.go index ec40891469..24b9f679ec 100644 --- a/be1-go/channel/chirp/mod_test.go +++ b/be1-go/channel/chirp/mod_test.go @@ -404,6 +404,15 @@ func Test_Delete_Chirp(t *testing.T) { require.Nil(t, err) checkData64Delete := base64.URLEncoding.EncodeToString(checkDataBufDelete) + var notifyAddMessage messagedata.ChirpNotifyAdd + err = msg[0].UnmarshalData(¬ifyAddMessage) + require.NoError(t, err) + require.Equal(t, checkDataAdd.Object, notifyAddMessage.Object) + require.Equal(t, checkDataAdd.Action, notifyAddMessage.Action) + require.Equal(t, checkDataAdd.ChirpID, notifyAddMessage.ChirpID) + require.Equal(t, checkDataAdd.Channel, notifyAddMessage.Channel) + require.Equal(t, checkDataAdd.Timestamp, notifyAddMessage.Timestamp) + // check if the data on the general is the same as the one we sent require.Equal(t, checkData64Add, msg[0].Data) require.Equal(t, checkData64Delete, msg[1].Data) From 76838481fbdff8e720c54bc84a9bcb1a0d789ced Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 10 Jan 2023 15:42:54 +0100 Subject: [PATCH 017/121] add delay between messages in test delete chirp --- be1-go/channel/chirp/mod_test.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/be1-go/channel/chirp/mod_test.go b/be1-go/channel/chirp/mod_test.go index 24b9f679ec..30b2d53060 100644 --- a/be1-go/channel/chirp/mod_test.go +++ b/be1-go/channel/chirp/mod_test.go @@ -348,6 +348,8 @@ func Test_Delete_Chirp(t *testing.T) { // publish add chirp message require.NoError(t, cha.Publish(pub, socket.ClientSocket{})) + time.Sleep(time.Millisecond) + // create delete chirp message file = filepath.Join(relativeMsgDataExamplePath, "chirp_delete_publish", "chirp_delete_publish.json") @@ -404,15 +406,6 @@ func Test_Delete_Chirp(t *testing.T) { require.Nil(t, err) checkData64Delete := base64.URLEncoding.EncodeToString(checkDataBufDelete) - var notifyAddMessage messagedata.ChirpNotifyAdd - err = msg[0].UnmarshalData(¬ifyAddMessage) - require.NoError(t, err) - require.Equal(t, checkDataAdd.Object, notifyAddMessage.Object) - require.Equal(t, checkDataAdd.Action, notifyAddMessage.Action) - require.Equal(t, checkDataAdd.ChirpID, notifyAddMessage.ChirpID) - require.Equal(t, checkDataAdd.Channel, notifyAddMessage.Channel) - require.Equal(t, checkDataAdd.Timestamp, notifyAddMessage.Timestamp) - // check if the data on the general is the same as the one we sent require.Equal(t, checkData64Add, msg[0].Data) require.Equal(t, checkData64Delete, msg[1].Data) From 1368040891daaab7b267c3fb8ca3c47715071e2f Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Thu, 12 Jan 2023 16:18:35 +0100 Subject: [PATCH 018/121] Add auto-complete to the send&receive screen --- fe1-web/package-lock.json | 56 ++++ fe1-web/package.json | 2 + fe1-web/src/core/components/Input.tsx | 10 +- fe1-web/src/core/components/ScannerInput.tsx | 104 +++++++- .../screens/SendReceive/SendReceive.tsx | 29 +- .../__snapshots__/SendReceive.test.tsx.snap | 248 ++++++++++++++---- 6 files changed, 386 insertions(+), 63 deletions(-) diff --git a/fe1-web/package-lock.json b/fe1-web/package-lock.json index 5216473ac3..55a5a3354d 100644 --- a/fe1-web/package-lock.json +++ b/fe1-web/package-lock.json @@ -22,6 +22,7 @@ "@reduxjs/toolkit": "^1.8.5", "@rneui/base": "^4.0.0-rc.6", "@rneui/themed": "^4.0.0-rc.6", + "@types/react-native-autocomplete-input": "^5.1.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "base64url": "^3.0.1", @@ -41,6 +42,7 @@ "react-dom": "18.0.0", "react-native": "^0.69.6", "react-native-action-sheet": "^2.2.0", + "react-native-autocomplete-input": "^5.2.0", "react-native-gesture-handler": "~2.5.0", "react-native-pager-view": "5.4.24", "react-native-reanimated": "~2.9.1", @@ -9855,6 +9857,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-native-autocomplete-input": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-5.1.0.tgz", + "integrity": "sha512-ICXh3AM5aM2z9yFMg0jX5K+rVfFx+l8WxBt7R73Jmyeqe6/7+ynoC/uEuQiqWnMqG1+WirvEoKdxlcB1RwzPVQ==", + "dependencies": { + "@types/react": "*", + "@types/react-native": "*" + } + }, "node_modules/@types/react-native-vector-icons": { "version": "6.4.11", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.11.tgz", @@ -14762,6 +14773,16 @@ "integrity": "sha512-MHidOOnCHGlZDKsI21+mbIIhf4Fff+hhCTB7gtVg4uoIqjcrTZc5v6M+GS2zVI0sV7PqK415rb8XaOSQsQkHOw==", "dev": true }, + "node_modules/deprecated-react-native-prop-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", + "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", + "dependencies": { + "@react-native/normalize-color": "*", + "invariant": "*", + "prop-types": "*" + } + }, "node_modules/dequal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dequal/-/dequal-1.0.1.tgz", @@ -32463,6 +32484,14 @@ "resolved": "https://registry.npmjs.org/react-native-action-sheet/-/react-native-action-sheet-2.2.0.tgz", "integrity": "sha512-4lsuxH+Cn3/aUEs1VCwqvLhEFyXNqYTkT67CzgTwlifD9Ij4OPQAIs8D+HUD9zBvWc4NtT6cyG1lhArPVMQeVw==" }, + "node_modules/react-native-autocomplete-input": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-native-autocomplete-input/-/react-native-autocomplete-input-5.2.0.tgz", + "integrity": "sha512-fme1UmYdYbPO8hnxnou5Nis0ZSWYEQiFyLdlyx779VBCDdYOmQVGQaD6JZLIk0P7B5Ulnov/axjEMWDC2iS/yA==", + "dependencies": { + "deprecated-react-native-prop-types": "^2.3.0" + } + }, "node_modules/react-native-codegen": { "version": "0.69.2", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.2.tgz", @@ -48810,6 +48839,15 @@ "@types/react": "*" } }, + "@types/react-native-autocomplete-input": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-5.1.0.tgz", + "integrity": "sha512-ICXh3AM5aM2z9yFMg0jX5K+rVfFx+l8WxBt7R73Jmyeqe6/7+ynoC/uEuQiqWnMqG1+WirvEoKdxlcB1RwzPVQ==", + "requires": { + "@types/react": "*", + "@types/react-native": "*" + } + }, "@types/react-native-vector-icons": { "version": "6.4.11", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.11.tgz", @@ -52724,6 +52762,16 @@ "integrity": "sha512-MHidOOnCHGlZDKsI21+mbIIhf4Fff+hhCTB7gtVg4uoIqjcrTZc5v6M+GS2zVI0sV7PqK415rb8XaOSQsQkHOw==", "dev": true }, + "deprecated-react-native-prop-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", + "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", + "requires": { + "@react-native/normalize-color": "*", + "invariant": "*", + "prop-types": "*" + } + }, "dequal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dequal/-/dequal-1.0.1.tgz", @@ -66804,6 +66852,14 @@ "resolved": "https://registry.npmjs.org/react-native-action-sheet/-/react-native-action-sheet-2.2.0.tgz", "integrity": "sha512-4lsuxH+Cn3/aUEs1VCwqvLhEFyXNqYTkT67CzgTwlifD9Ij4OPQAIs8D+HUD9zBvWc4NtT6cyG1lhArPVMQeVw==" }, + "react-native-autocomplete-input": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-native-autocomplete-input/-/react-native-autocomplete-input-5.2.0.tgz", + "integrity": "sha512-fme1UmYdYbPO8hnxnou5Nis0ZSWYEQiFyLdlyx779VBCDdYOmQVGQaD6JZLIk0P7B5Ulnov/axjEMWDC2iS/yA==", + "requires": { + "deprecated-react-native-prop-types": "^2.3.0" + } + }, "react-native-codegen": { "version": "0.69.2", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.2.tgz", diff --git a/fe1-web/package.json b/fe1-web/package.json index 7d4a58d106..3a4a29b606 100644 --- a/fe1-web/package.json +++ b/fe1-web/package.json @@ -41,6 +41,7 @@ "@reduxjs/toolkit": "^1.8.5", "@rneui/base": "^4.0.0-rc.6", "@rneui/themed": "^4.0.0-rc.6", + "@types/react-native-autocomplete-input": "^5.1.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "base64url": "^3.0.1", @@ -60,6 +61,7 @@ "react-dom": "18.0.0", "react-native": "^0.69.6", "react-native-action-sheet": "^2.2.0", + "react-native-autocomplete-input": "^5.2.0", "react-native-gesture-handler": "~2.5.0", "react-native-pager-view": "5.4.24", "react-native-reanimated": "~2.9.1", diff --git a/fe1-web/src/core/components/Input.tsx b/fe1-web/src/core/components/Input.tsx index 8948972a4e..b8db9eea36 100644 --- a/fe1-web/src/core/components/Input.tsx +++ b/fe1-web/src/core/components/Input.tsx @@ -4,7 +4,7 @@ import { StyleSheet, TextInput, View } from 'react-native'; import { Border, Color, Spacing, Typography } from 'core/styles'; -const styles = StyleSheet.create({ +export const inputStyleSheet = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', @@ -32,18 +32,18 @@ const styles = StyleSheet.create({ const Input = (props: IPropTypes) => { const { value, placeholder, onChange, enabled, negative, testID } = props; - const inputStyles = [Typography.paragraph, styles.input]; + const inputStyles = [Typography.paragraph, inputStyleSheet.input]; if (!enabled) { - inputStyles.push(styles.disabled); + inputStyles.push(inputStyleSheet.disabled); } if (negative) { - inputStyles.push(styles.negative); + inputStyles.push(inputStyleSheet.negative); } return ( - + { - const { value, placeholder, onChange, onPress, enabled, testID } = props; +const ScannerInput = ({ + value, + suggestions, + placeholder, + onChange, + onPress, + onFocus, + onBlur, + enabled, + testID, +}: IPropTypes) => { + const inputStyles = [Typography.paragraph, inputStyleSheet.input]; + + if (!enabled) { + inputStyles.push(inputStyleSheet.disabled); + } return ( - + i, + renderItem: ({ item, index }) => ( + onChange(item)} + containerStyle={ + index === (suggestions || []).length - 1 + ? [styles.suggestionContainer, styles.suggestionContainerLast] + : styles.suggestionContainer + }> + + {item} + + + ), + }} + testID={testID || undefined} + style={inputStyles} + inputContainerStyle={[inputStyleSheet.container, styles.inputContainer]} + listContainerStyle={styles.suggestionsContainer} + /> + + {/* + /> */} @@ -43,15 +127,21 @@ const propTypes = { enabled: PropTypes.bool, placeholder: PropTypes.string, value: PropTypes.string.isRequired, + suggestions: PropTypes.arrayOf(PropTypes.string.isRequired), onChange: PropTypes.func, onPress: PropTypes.func.isRequired, + onFocus: PropTypes.func, + onBlur: PropTypes.func, testID: PropTypes.string, }; ScannerInput.propTypes = propTypes; ScannerInput.defaultProps = { placeholder: '', + suggestions: [], enabled: true, onChange: undefined, + onFocus: undefined, + onBlur: undefined, testID: undefined, }; diff --git a/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx b/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx index af25ae0bba..acc8a2290a 100644 --- a/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx +++ b/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx @@ -2,7 +2,7 @@ import { CompositeScreenProps, useNavigation, useRoute } from '@react-navigation import { StackScreenProps } from '@react-navigation/stack'; import * as Clipboard from 'expo-clipboard'; import React, { useEffect, useMemo, useState } from 'react'; -import { Modal, StyleSheet, Text, TextStyle, View } from 'react-native'; +import { Modal, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native'; import { ScrollView, TouchableOpacity, @@ -37,6 +37,9 @@ const styles = StyleSheet.create({ color: Color.inactive, textAlign: 'center', } as TextStyle, + beneficiaries: { + zIndex: 99, + } as ViewStyle, }); const SendReceive = () => { @@ -51,12 +54,14 @@ const SendReceive = () => { [serializedRollCallId], ); - // will be undefined for the organizer + // will be undefined for coinbase transactions const rollCallToken = DigitalCashHooks.useRollCallTokenByRollCallId(laoId, rollCallId); + const rollCall = DigitalCashHooks.useRollCallById(rollCallId); + const allRollCalls = DigitalCashHooks.useRollCallsByLaoId(laoId); - const allClosedRollCall = useMemo( + const allClosedRollCalls = useMemo( () => Object.values(allRollCalls).filter((rc) => rc.status === 2), [allRollCalls], ); @@ -71,9 +76,21 @@ const SendReceive = () => { const selectedRollCall = DigitalCashHooks.useRollCallById(selectedRollCallId); const [beneficiary, setBeneficiary] = useState(''); + const [beneficiaryFocused, setBeneficiaryFocused] = useState(false); const [amount, setAmount] = useState(''); const [error, setError] = useState(''); + const suggestedBeneficiaries = useMemo(() => { + // do not show any suggestions if no text has been entered + if (!beneficiaryFocused && beneficiary.trim().length === 0) { + return []; + } + + return (rollCall?.attendees || []) + .map((key) => key.toString()) + .filter((key) => key.startsWith(beneficiary)); + }, [beneficiary, beneficiaryFocused, rollCall]); + if (!(rollCallId || isCoinbase)) { throw new Error( 'The source of a transaction must either be a roll call token or it must be a coinbase transaction', @@ -198,7 +215,7 @@ const SendReceive = () => { {STRINGS.digital_cash_wallet_transaction_description} - + {STRINGS.digital_cash_wallet_beneficiary} @@ -215,7 +232,7 @@ const SendReceive = () => { value: '', label: STRINGS.digital_cash_wallet_issue_single_beneficiary, }, - ...allClosedRollCall.map((rc) => ({ + ...allClosedRollCalls.map((rc) => ({ value: rc.id.valueOf(), label: `${STRINGS.digital_cash_wallet_issue_all_attendees} "${rc.name}"`, })), @@ -225,6 +242,7 @@ const SendReceive = () => { {serializedSelectedRollCallId === '' && ( { navigation.navigate(STRINGS.navigation_digital_cash_wallet_scanner, { @@ -232,6 +250,7 @@ const SendReceive = () => { isCoinbase: isCoinbase, }); }} + onFocus={() => setBeneficiaryFocused(true)} placeholder={STRINGS.digital_cash_wallet_beneficiary_placeholder} /> )} diff --git a/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap b/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap index 1776f840d4..ee59f43a19 100644 --- a/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap +++ b/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap @@ -380,7 +380,13 @@ exports[`SendReceive renders correctly 1`] = ` > You can send cash by entering the public key of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen. - + @@ -411,37 +418,108 @@ exports[`SendReceive renders correctly 1`] = ` style={ Object { "flex": 1, - "flexDirection": "row", } } > - + > + + + + You can send cash by entering the public key of the beneficiary below and choosing the amount of cash you would like to transfer. To receive money you canshow your PoP token to the sender. To access the QR code of your PoP token, tab the QRcode icon in the top right of this screen. - + @@ -1075,37 +1160,108 @@ exports[`SendReceive renders correctly with passed scanned pop token 1`] = ` style={ Object { "flex": 1, - "flexDirection": "row", } } > - + > + + + + Date: Thu, 19 Jan 2023 18:26:42 +0100 Subject: [PATCH 019/121] Replace autocomplete dependency with own component --- fe1-web/package-lock.json | 56 ---- fe1-web/package.json | 2 - .../src/core/components/AutocompleteInput.tsx | 123 +++++++++ fe1-web/src/core/components/Input.tsx | 8 +- fe1-web/src/core/components/ScannerInput.tsx | 67 +---- .../__tests__/ScannerInput.test.tsx | 13 + .../__snapshots__/ScannerInput.test.tsx.snap | 50 ++++ fe1-web/src/core/components/index.ts | 2 + .../screens/SendReceive/SendReceive.tsx | 10 +- .../__snapshots__/SendReceive.test.tsx.snap | 252 +++++------------- 10 files changed, 286 insertions(+), 297 deletions(-) create mode 100644 fe1-web/src/core/components/AutocompleteInput.tsx create mode 100644 fe1-web/src/core/components/__tests__/ScannerInput.test.tsx create mode 100644 fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap diff --git a/fe1-web/package-lock.json b/fe1-web/package-lock.json index 55a5a3354d..5216473ac3 100644 --- a/fe1-web/package-lock.json +++ b/fe1-web/package-lock.json @@ -22,7 +22,6 @@ "@reduxjs/toolkit": "^1.8.5", "@rneui/base": "^4.0.0-rc.6", "@rneui/themed": "^4.0.0-rc.6", - "@types/react-native-autocomplete-input": "^5.1.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "base64url": "^3.0.1", @@ -42,7 +41,6 @@ "react-dom": "18.0.0", "react-native": "^0.69.6", "react-native-action-sheet": "^2.2.0", - "react-native-autocomplete-input": "^5.2.0", "react-native-gesture-handler": "~2.5.0", "react-native-pager-view": "5.4.24", "react-native-reanimated": "~2.9.1", @@ -9857,15 +9855,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-native-autocomplete-input": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-5.1.0.tgz", - "integrity": "sha512-ICXh3AM5aM2z9yFMg0jX5K+rVfFx+l8WxBt7R73Jmyeqe6/7+ynoC/uEuQiqWnMqG1+WirvEoKdxlcB1RwzPVQ==", - "dependencies": { - "@types/react": "*", - "@types/react-native": "*" - } - }, "node_modules/@types/react-native-vector-icons": { "version": "6.4.11", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.11.tgz", @@ -14773,16 +14762,6 @@ "integrity": "sha512-MHidOOnCHGlZDKsI21+mbIIhf4Fff+hhCTB7gtVg4uoIqjcrTZc5v6M+GS2zVI0sV7PqK415rb8XaOSQsQkHOw==", "dev": true }, - "node_modules/deprecated-react-native-prop-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", - "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", - "dependencies": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "node_modules/dequal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dequal/-/dequal-1.0.1.tgz", @@ -32484,14 +32463,6 @@ "resolved": "https://registry.npmjs.org/react-native-action-sheet/-/react-native-action-sheet-2.2.0.tgz", "integrity": "sha512-4lsuxH+Cn3/aUEs1VCwqvLhEFyXNqYTkT67CzgTwlifD9Ij4OPQAIs8D+HUD9zBvWc4NtT6cyG1lhArPVMQeVw==" }, - "node_modules/react-native-autocomplete-input": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-native-autocomplete-input/-/react-native-autocomplete-input-5.2.0.tgz", - "integrity": "sha512-fme1UmYdYbPO8hnxnou5Nis0ZSWYEQiFyLdlyx779VBCDdYOmQVGQaD6JZLIk0P7B5Ulnov/axjEMWDC2iS/yA==", - "dependencies": { - "deprecated-react-native-prop-types": "^2.3.0" - } - }, "node_modules/react-native-codegen": { "version": "0.69.2", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.2.tgz", @@ -48839,15 +48810,6 @@ "@types/react": "*" } }, - "@types/react-native-autocomplete-input": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-5.1.0.tgz", - "integrity": "sha512-ICXh3AM5aM2z9yFMg0jX5K+rVfFx+l8WxBt7R73Jmyeqe6/7+ynoC/uEuQiqWnMqG1+WirvEoKdxlcB1RwzPVQ==", - "requires": { - "@types/react": "*", - "@types/react-native": "*" - } - }, "@types/react-native-vector-icons": { "version": "6.4.11", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.11.tgz", @@ -52762,16 +52724,6 @@ "integrity": "sha512-MHidOOnCHGlZDKsI21+mbIIhf4Fff+hhCTB7gtVg4uoIqjcrTZc5v6M+GS2zVI0sV7PqK415rb8XaOSQsQkHOw==", "dev": true }, - "deprecated-react-native-prop-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", - "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", - "requires": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "dequal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dequal/-/dequal-1.0.1.tgz", @@ -66852,14 +66804,6 @@ "resolved": "https://registry.npmjs.org/react-native-action-sheet/-/react-native-action-sheet-2.2.0.tgz", "integrity": "sha512-4lsuxH+Cn3/aUEs1VCwqvLhEFyXNqYTkT67CzgTwlifD9Ij4OPQAIs8D+HUD9zBvWc4NtT6cyG1lhArPVMQeVw==" }, - "react-native-autocomplete-input": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-native-autocomplete-input/-/react-native-autocomplete-input-5.2.0.tgz", - "integrity": "sha512-fme1UmYdYbPO8hnxnou5Nis0ZSWYEQiFyLdlyx779VBCDdYOmQVGQaD6JZLIk0P7B5Ulnov/axjEMWDC2iS/yA==", - "requires": { - "deprecated-react-native-prop-types": "^2.3.0" - } - }, "react-native-codegen": { "version": "0.69.2", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.2.tgz", diff --git a/fe1-web/package.json b/fe1-web/package.json index 3a4a29b606..7d4a58d106 100644 --- a/fe1-web/package.json +++ b/fe1-web/package.json @@ -41,7 +41,6 @@ "@reduxjs/toolkit": "^1.8.5", "@rneui/base": "^4.0.0-rc.6", "@rneui/themed": "^4.0.0-rc.6", - "@types/react-native-autocomplete-input": "^5.1.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "base64url": "^3.0.1", @@ -61,7 +60,6 @@ "react-dom": "18.0.0", "react-native": "^0.69.6", "react-native-action-sheet": "^2.2.0", - "react-native-autocomplete-input": "^5.2.0", "react-native-gesture-handler": "~2.5.0", "react-native-pager-view": "5.4.24", "react-native-reanimated": "~2.9.1", diff --git a/fe1-web/src/core/components/AutocompleteInput.tsx b/fe1-web/src/core/components/AutocompleteInput.tsx new file mode 100644 index 0000000000..3dba7e2bc5 --- /dev/null +++ b/fe1-web/src/core/components/AutocompleteInput.tsx @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, { useCallback } from 'react'; +import { View, Text, StyleSheet, ListRenderItemInfo } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; + +import { Border, Color, Spacing, Typography } from 'core/styles'; + +import Input from './Input'; +import PoPTouchableOpacity from './PoPTouchableOpacity'; + +const styles = StyleSheet.create({ + inputContainer: { + borderWidth: 0, + }, + suggestionsContainer: { + position: 'absolute', + zIndex: 100, + top: '100%', + left: 0, + right: 0, + maxHeight: 200, + backgroundColor: Color.contrast, + borderRadius: Border.inputRadius, + shadowColor: Color.primary, + shadowOpacity: 0.1, + shadowRadius: 3, + shadowOffset: { + height: 5, + width: 5, + }, + }, + suggestionContainer: { + paddingVertical: Spacing.x05, + paddingHorizontal: Spacing.x1, + borderBottomColor: Color.separator, + borderBottomWidth: 1, + }, + suggestionContainerLast: { + borderRadius: Border.inputRadius, + }, +}); + +const AutocompleteInput = ({ + value, + suggestions, + showResults, + onChange, + onBlur, + onFocus, + placeholder, + enabled, + testID, +}: IPropTypes) => { + const lastSuggestionIndex = suggestions ? suggestions.length - 1 : 0; + + const renderSuggestion = useCallback( + ({ item: suggestion, index }: ListRenderItemInfo) => ( + onChange(suggestion)} + containerStyle={ + index === lastSuggestionIndex + ? [styles.suggestionContainer, styles.suggestionContainerLast] + : styles.suggestionContainer + }> + + {suggestion} + + + ), + [onChange, lastSuggestionIndex], + ); + + return ( + + + + + {showResults && ( + + index.toString()} + /> + + )} + + ); +}; + +const propTypes = { + value: PropTypes.string.isRequired, + suggestions: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, + onChange: PropTypes.func.isRequired, + placeholder: PropTypes.string, + enabled: PropTypes.bool, + showResults: PropTypes.bool, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + testID: PropTypes.string, +}; + +AutocompleteInput.propTypes = propTypes; +AutocompleteInput.defaultProps = { + placeholder: '', + enabled: true, + showResults: false, + onFocus: undefined, + onBlur: undefined, + testID: undefined, +}; + +type IPropTypes = PropTypes.InferProps; + +export default AutocompleteInput; diff --git a/fe1-web/src/core/components/Input.tsx b/fe1-web/src/core/components/Input.tsx index b8db9eea36..718cacd921 100644 --- a/fe1-web/src/core/components/Input.tsx +++ b/fe1-web/src/core/components/Input.tsx @@ -30,7 +30,7 @@ export const inputStyleSheet = StyleSheet.create({ }); const Input = (props: IPropTypes) => { - const { value, placeholder, onChange, enabled, negative, testID } = props; + const { value, placeholder, onChange, onFocus, onBlur, enabled, negative, testID } = props; const inputStyles = [Typography.paragraph, inputStyleSheet.input]; @@ -51,6 +51,8 @@ const Input = (props: IPropTypes) => { value={value} placeholder={placeholder || ''} onChangeText={enabled ? onChange : undefined} + onFocus={onFocus || undefined} + onBlur={onBlur || undefined} testID={testID || undefined} /> @@ -61,6 +63,8 @@ const propTypes = { placeholder: PropTypes.string, value: PropTypes.string.isRequired, onChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, enabled: PropTypes.bool, negative: PropTypes.bool, testID: PropTypes.string, @@ -69,6 +73,8 @@ Input.propTypes = propTypes; Input.defaultProps = { placeholder: '', onChange: undefined, + onFocus: undefined, + onBlur: undefined, enabled: true, negative: false, testID: undefined, diff --git a/fe1-web/src/core/components/ScannerInput.tsx b/fe1-web/src/core/components/ScannerInput.tsx index 7c0e8a9920..7c196d69da 100644 --- a/fe1-web/src/core/components/ScannerInput.tsx +++ b/fe1-web/src/core/components/ScannerInput.tsx @@ -1,14 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import AutocompleteInput from 'react-native-autocomplete-input'; +import { StyleSheet, View } from 'react-native'; -import { Border, Color, Spacing, Typography } from 'core/styles'; +import { Color, Spacing, Typography } from 'core/styles'; +import AutocompleteInput from './AutocompleteInput'; import { inputStyleSheet } from './Input'; import PoPButton from './PoPButton'; import PoPIcon from './PoPIcon'; -import PoPTouchableOpacity from './PoPTouchableOpacity'; const styles = StyleSheet.create({ container: { @@ -22,34 +21,6 @@ const styles = StyleSheet.create({ autocompleteContainer: { flex: 1, }, - inputContainer: { - borderWidth: 0, - }, - suggestionsContainer: { - position: 'absolute', - zIndex: 100, - top: '100%', - left: 0, - right: 0, - backgroundColor: Color.contrast, - borderRadius: Border.inputRadius, - shadowColor: Color.primary, - shadowOpacity: 0.1, - shadowRadius: 3, - shadowOffset: { - height: 5, - width: 5, - }, - }, - suggestionContainer: { - paddingVertical: Spacing.x05, - paddingHorizontal: Spacing.x1, - borderBottomColor: Color.separator, - borderBottomWidth: 1, - }, - suggestionContainerLast: { - borderRadius: Border.inputRadius, - }, }); const ScannerInput = ({ @@ -73,38 +44,18 @@ const ScannerInput = ({ i, - renderItem: ({ item, index }) => ( - onChange(item)} - containerStyle={ - index === (suggestions || []).length - 1 - ? [styles.suggestionContainer, styles.suggestionContainerLast] - : styles.suggestionContainer - }> - - {item} - - - ), - }} + onChange={onChange} testID={testID || undefined} - style={inputStyles} - inputContainerStyle={[inputStyleSheet.container, styles.inputContainer]} - listContainerStyle={styles.suggestionsContainer} /> {/* { + it('renders correctly', () => { + const { toJSON } = render( + {}} suggestions={['aaa', 'aab', 'aac']} />, + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap b/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap new file mode 100644 index 0000000000..ef348c7854 --- /dev/null +++ b/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AutocompleteInput renders correctly 1`] = ` + + + + + + + +`; diff --git a/fe1-web/src/core/components/index.ts b/fe1-web/src/core/components/index.ts index 7b56fa339c..66e41968f5 100644 --- a/fe1-web/src/core/components/index.ts +++ b/fe1-web/src/core/components/index.ts @@ -5,6 +5,7 @@ * this is the module in which the user interface component should be defined. */ +export { default as AutocompleteInput } from './AutocompleteInput'; export { default as CameraButton } from './CameraButton'; export { default as CheckboxList } from './CheckboxList'; export { default as ConfirmModal } from './ConfirmModal'; @@ -19,6 +20,7 @@ export { default as ProfileIcon } from './ProfileIcon'; export { default as QRCode } from './QRCode'; export { default as RecorderButton } from './RecorderButton'; export { default as RemovableTextInput } from './RemovableTextInput'; +export { default as ScannerInput } from './ScannerInput'; export { default as TextInputLine } from './TextInputLine'; export { default as TextInputList } from './TextInputList'; export { default as TimeDisplay } from './TimeDisplay'; diff --git a/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx b/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx index acc8a2290a..9adc6e2306 100644 --- a/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx +++ b/fe1-web/src/features/digital-cash/screens/SendReceive/SendReceive.tsx @@ -10,9 +10,15 @@ import { } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; -import { DropdownSelector, Input, PoPIcon, PoPTextButton, QRCode } from 'core/components'; +import { + ScannerInput, + DropdownSelector, + Input, + PoPIcon, + PoPTextButton, + QRCode, +} from 'core/components'; import ModalHeader from 'core/components/ModalHeader'; -import ScannerInput from 'core/components/ScannerInput'; import ScreenWrapper from 'core/components/ScreenWrapper'; import { KeyPairStore } from 'core/keypair'; import { AppParamList } from 'core/navigation/typing/AppParamList'; diff --git a/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap b/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap index ee59f43a19..eddc2c1497 100644 --- a/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap +++ b/fe1-web/src/features/digital-cash/screens/SendReceive/__tests__/__snapshots__/SendReceive.test.tsx.snap @@ -421,103 +421,51 @@ exports[`SendReceive renders correctly 1`] = ` } } > - + - + + value="" + /> + @@ -1163,103 +1111,51 @@ exports[`SendReceive renders correctly with passed scanned pop token 1`] = ` } } > - + - + + value="some pop token" + /> + From 9584e25f96015e236b28c37b8faf000e7bb670e9 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Thu, 19 Jan 2023 20:03:53 +0100 Subject: [PATCH 020/121] Make fe-1 wallet consistent with fe-2 --- .../components/RollCallTokensDropDown.tsx | 51 --- .../wallet/components/RollCallWalletItems.tsx | 61 --- .../{RollCallWalletItem.tsx => Token.tsx} | 11 +- .../features/wallet/components/TokenList.tsx | 107 +++++ .../__tests__/RollCallTokensDropDown.test.tsx | 30 -- ...alletItems.test.tsx => TokenList.test.tsx} | 6 +- .../RollCallTokensDropDown.test.tsx.snap | 40 -- ....test.tsx.snap => TokenList.test.tsx.snap} | 0 .../src/features/wallet/components/index.ts | 3 - .../features/wallet/screens/WalletHome.tsx | 4 +- .../__snapshots__/WalletHome.test.tsx.snap | 392 ++++++++++-------- fe1-web/src/resources/strings.ts | 4 +- 12 files changed, 332 insertions(+), 377 deletions(-) delete mode 100644 fe1-web/src/features/wallet/components/RollCallTokensDropDown.tsx delete mode 100644 fe1-web/src/features/wallet/components/RollCallWalletItems.tsx rename fe1-web/src/features/wallet/components/{RollCallWalletItem.tsx => Token.tsx} (88%) create mode 100644 fe1-web/src/features/wallet/components/TokenList.tsx delete mode 100644 fe1-web/src/features/wallet/components/__tests__/RollCallTokensDropDown.test.tsx rename fe1-web/src/features/wallet/components/__tests__/{RollCallWalletItems.test.tsx => TokenList.test.tsx} (88%) delete mode 100644 fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallTokensDropDown.test.tsx.snap rename fe1-web/src/features/wallet/components/__tests__/__snapshots__/{RollCallWalletItems.test.tsx.snap => TokenList.test.tsx.snap} (100%) delete mode 100644 fe1-web/src/features/wallet/components/index.ts diff --git a/fe1-web/src/features/wallet/components/RollCallTokensDropDown.tsx b/fe1-web/src/features/wallet/components/RollCallTokensDropDown.tsx deleted file mode 100644 index 28785c9658..0000000000 --- a/fe1-web/src/features/wallet/components/RollCallTokensDropDown.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Picker } from '@react-native-picker/picker'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { StyleSheet } from 'react-native'; - -import { RollCallToken } from 'core/objects'; - -const styles = StyleSheet.create({ - pickerStyle: { - maxHeight: 40, - width: 'fit-content', - maxWidth: '90%', - fontSize: 20, - textAlign: 'center', - margin: 'auto', - }, -}); -const RollCallTokensDropDown = (props: IPropTypes) => { - const { onIndexChange, rollCallTokens, selectedTokenIndex } = props; - - const options = rollCallTokens.map((rc, index) => { - return ( - - ); - }); - - return ( - { - onIndexChange(itemIndex); - }} - style={styles.pickerStyle} - selectedValue={selectedTokenIndex}> - {options} - - ); -}; - -const propTypes = { - onIndexChange: PropTypes.func.isRequired, - rollCallTokens: PropTypes.arrayOf(PropTypes.instanceOf(RollCallToken).isRequired).isRequired, - selectedTokenIndex: PropTypes.number.isRequired, -}; -RollCallTokensDropDown.propTypes = propTypes; -type IPropTypes = PropTypes.InferProps; - -export default RollCallTokensDropDown; diff --git a/fe1-web/src/features/wallet/components/RollCallWalletItems.tsx b/fe1-web/src/features/wallet/components/RollCallWalletItems.tsx deleted file mode 100644 index bcb8005cbb..0000000000 --- a/fe1-web/src/features/wallet/components/RollCallWalletItems.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ListItem } from '@rneui/themed'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { View } from 'react-native'; - -import { PoPIcon } from 'core/components'; -import { Hash } from 'core/objects'; -import { Color, Icon, List, Typography } from 'core/styles'; -import STRINGS from 'resources/strings'; - -import { WalletHooks } from '../hooks'; -import RollCallWalletItem from './RollCallWalletItem'; - -const RollCallWalletItems = ({ laoId }: IPropTypes) => { - const tokens = WalletHooks.useRollCallTokensByLaoId(laoId); - - if (tokens.length > 0) { - return ( - <> - {tokens.map((token, idx) => ( - // isFirstItem and isLastItem have to be refactored in the future - // since it is not known what other items other features add - - ))} - - ); - } - - const listStyles = List.getListItemStyles(false, true); - - return ( - - - - - - - {STRINGS.wallet_home_rollcall_no_pop_tokens} - - - {STRINGS.wallet_home_rollcall_no_pop_tokens_description} - - - - ); -}; - -const propTypes = { - laoId: PropTypes.instanceOf(Hash).isRequired, -}; - -RollCallWalletItems.propTypes = propTypes; - -type IPropTypes = PropTypes.InferProps; - -export default RollCallWalletItems; diff --git a/fe1-web/src/features/wallet/components/RollCallWalletItem.tsx b/fe1-web/src/features/wallet/components/Token.tsx similarity index 88% rename from fe1-web/src/features/wallet/components/RollCallWalletItem.tsx rename to fe1-web/src/features/wallet/components/Token.tsx index 09ee2a4dc4..e5689e7595 100644 --- a/fe1-web/src/features/wallet/components/RollCallWalletItem.tsx +++ b/fe1-web/src/features/wallet/components/Token.tsx @@ -18,7 +18,7 @@ type NavigationProps = CompositeScreenProps< StackScreenProps >; -const RollCallWalletItem = ({ rollCallToken, isFirstItem, isLastItem }: IPropTypes) => { +const RollCallWalletItem = ({ rollCallToken, subtitle, isFirstItem, isLastItem }: IPropTypes) => { const navigation = useNavigation(); const listStyles = List.getListItemStyles(isFirstItem, isLastItem); @@ -40,9 +40,7 @@ const RollCallWalletItem = ({ rollCallToken, isFirstItem, isLastItem }: IPropTyp {rollCallToken.rollCallName} - - {STRINGS.wallet_home_rollcall_pop_token} - + {subtitle} @@ -50,6 +48,7 @@ const RollCallWalletItem = ({ rollCallToken, isFirstItem, isLastItem }: IPropTyp }; const propTypes = { + subtitle: PropTypes.string, rollCallToken: PropTypes.instanceOf(RollCallToken).isRequired, isFirstItem: PropTypes.bool.isRequired, isLastItem: PropTypes.bool.isRequired, @@ -57,6 +56,10 @@ const propTypes = { RollCallWalletItem.propTypes = propTypes; +RollCallWalletItem.defaultProps = { + subtitle: '', +}; + type IPropTypes = PropTypes.InferProps; export default RollCallWalletItem; diff --git a/fe1-web/src/features/wallet/components/TokenList.tsx b/fe1-web/src/features/wallet/components/TokenList.tsx new file mode 100644 index 0000000000..9bd1b2a8a5 --- /dev/null +++ b/fe1-web/src/features/wallet/components/TokenList.tsx @@ -0,0 +1,107 @@ +import { ListItem } from '@rneui/themed'; +import PropTypes from 'prop-types'; +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { PoPIcon } from 'core/components'; +import { Hash } from 'core/objects'; +import { Color, Icon, List, Spacing, Typography } from 'core/styles'; +import STRINGS from 'resources/strings'; + +import { WalletHooks } from '../hooks'; +import Token from './Token'; + +const styles = StyleSheet.create({ + validToken: { + paddingBottom: Spacing.x1, + }, + title: { + paddingBottom: Spacing.x05, + }, +}); + +const TokenList = ({ laoId }: IPropTypes) => { + const tokens = WalletHooks.useRollCallTokensByLaoId(laoId); + const lastTokenizedRollCallId = WalletHooks.useCurrentLao().last_tokenized_roll_call_id; + + const lastTokenizedRollCall = useMemo( + () => + lastTokenizedRollCallId + ? tokens.find((t) => t.rollCallId.equals(lastTokenizedRollCallId)) + : undefined, + [tokens, lastTokenizedRollCallId], + ); + + const previousTokens = useMemo( + () => + tokens.filter( + (t) => !lastTokenizedRollCallId || !t.rollCallId.equals(lastTokenizedRollCallId), + ), + [tokens, lastTokenizedRollCallId], + ); + + if (tokens.length > 0) { + return ( + + {lastTokenizedRollCall && ( + + + {STRINGS.wallet_home_rollcall_current_pop_tokens} + + + + )} + {previousTokens.length > 0 && ( + + + {STRINGS.wallet_home_rollcall_previous_pop_tokens} + + {previousTokens.map((token, idx) => ( + // isFirstItem and isLastItem have to be refactored in the future + // since it is not known what other items other features add + + ))} + + )} + + ); + } + + const listStyles = List.getListItemStyles(false, true); + + return ( + + + + + + + {STRINGS.wallet_home_rollcall_no_pop_tokens} + + + {STRINGS.wallet_home_rollcall_no_pop_tokens_description} + + + + ); +}; + +const propTypes = { + laoId: PropTypes.instanceOf(Hash).isRequired, +}; + +TokenList.propTypes = propTypes; + +type IPropTypes = PropTypes.InferProps; + +export default TokenList; diff --git a/fe1-web/src/features/wallet/components/__tests__/RollCallTokensDropDown.test.tsx b/fe1-web/src/features/wallet/components/__tests__/RollCallTokensDropDown.test.tsx deleted file mode 100644 index a286d687d6..0000000000 --- a/fe1-web/src/features/wallet/components/__tests__/RollCallTokensDropDown.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { render } from '@testing-library/react-native'; -import React from 'react'; - -import { mockKeyPair, mockLao } from '__tests__/utils'; -import { Hash, PopToken, RollCallToken } from 'core/objects'; - -import { RollCallTokensDropDown } from '../index'; - -const mockToken = PopToken.fromState(mockKeyPair.toState()); -const mockArray = [ - new RollCallToken({ - token: mockToken, - laoId: mockLao.id, - rollCallId: new Hash('id'), - rollCallName: 'rcname', - }), -]; - -describe('RollCallTokensDropDown', () => { - it('renders correctly', () => { - const { toJSON } = render( - {}} - />, - ); - expect(toJSON()).toMatchSnapshot(); - }); -}); diff --git a/fe1-web/src/features/wallet/components/__tests__/RollCallWalletItems.test.tsx b/fe1-web/src/features/wallet/components/__tests__/TokenList.test.tsx similarity index 88% rename from fe1-web/src/features/wallet/components/__tests__/RollCallWalletItems.test.tsx rename to fe1-web/src/features/wallet/components/__tests__/TokenList.test.tsx index 3270753fec..7703dab390 100644 --- a/fe1-web/src/features/wallet/components/__tests__/RollCallWalletItems.test.tsx +++ b/fe1-web/src/features/wallet/components/__tests__/TokenList.test.tsx @@ -11,7 +11,7 @@ import { WALLET_FEATURE_IDENTIFIER, } from 'features/wallet/interface'; -import RollCallWalletItems from '../RollCallWalletItems'; +import TokenList from '../TokenList'; const contextValue = (useRollCallsByLaoId: Record) => ({ [WALLET_FEATURE_IDENTIFIER]: { @@ -27,7 +27,7 @@ const contextValue = (useRollCallsByLaoId: Record { it('renders correctly with roll calls', () => { - const Screen = () => ; + const Screen = () => ; const { toJSON } = render( @@ -38,7 +38,7 @@ describe('RollCallWalletItems', () => { }); it('renders correctly without roll calls', () => { - const Screen = () => ; + const Screen = () => ; const { toJSON } = render( diff --git a/fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallTokensDropDown.test.tsx.snap b/fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallTokensDropDown.test.tsx.snap deleted file mode 100644 index dedf152fea..0000000000 --- a/fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallTokensDropDown.test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RollCallTokensDropDown renders correctly 1`] = ` - - - -`; diff --git a/fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallWalletItems.test.tsx.snap b/fe1-web/src/features/wallet/components/__tests__/__snapshots__/TokenList.test.tsx.snap similarity index 100% rename from fe1-web/src/features/wallet/components/__tests__/__snapshots__/RollCallWalletItems.test.tsx.snap rename to fe1-web/src/features/wallet/components/__tests__/__snapshots__/TokenList.test.tsx.snap diff --git a/fe1-web/src/features/wallet/components/index.ts b/fe1-web/src/features/wallet/components/index.ts deleted file mode 100644 index f9b71992f3..0000000000 --- a/fe1-web/src/features/wallet/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import RollCallTokensDropDown from './RollCallTokensDropDown'; - -export { RollCallTokensDropDown }; diff --git a/fe1-web/src/features/wallet/screens/WalletHome.tsx b/fe1-web/src/features/wallet/screens/WalletHome.tsx index 2d8fdbb551..36df491c99 100644 --- a/fe1-web/src/features/wallet/screens/WalletHome.tsx +++ b/fe1-web/src/features/wallet/screens/WalletHome.tsx @@ -4,7 +4,7 @@ import { View } from 'react-native'; import ScreenWrapper from 'core/components/ScreenWrapper'; import { List } from 'core/styles'; -import RollCallWalletItems from '../components/RollCallWalletItems'; +import TokenList from '../components/TokenList'; import { WalletHooks } from '../hooks'; /** @@ -16,7 +16,7 @@ const WalletHome = () => { return ( - + ); diff --git a/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletHome.test.tsx.snap b/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletHome.test.tsx.snap index 621a927879..cfc2ca3de2 100644 --- a/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletHome.test.tsx.snap +++ b/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletHome.test.tsx.snap @@ -355,213 +355,241 @@ exports[`Wallet home renders correctly with a non empty wallet 1`] = ` } } > - - - + + - - {"name":"ios-qr-code","size":25,"color":"#000"} - - - + Previous tokens + - - myRollCall - - - A PoP token received in a roll call - - - - - + + {"name":"ios-qr-code","size":25,"color":"#000"} + + + + /> - + + myRollCall + + + + + + + + + + diff --git a/fe1-web/src/resources/strings.ts b/fe1-web/src/resources/strings.ts index 0fb1e1740d..b2f40ebc59 100644 --- a/fe1-web/src/resources/strings.ts +++ b/fe1-web/src/resources/strings.ts @@ -420,7 +420,9 @@ namespace STRINGS { /* --- Wallet Home Screen Strings --- */ export const wallet_home_roll_calls = 'Roll Calls'; export const wallet_home_toggle_debug = 'Toggle debug mode'; - export const wallet_home_rollcall_pop_token = 'A PoP token received in a roll call'; + export const wallet_home_rollcall_current_pop_tokens = 'Current tokens'; + export const wallet_home_rollcall_previous_pop_tokens = 'Previous tokens'; + export const wallet_home_rollcall_pop_token_valid = 'Current'; export const wallet_home_rollcall_no_pop_tokens = 'No PoP tokens'; export const wallet_home_rollcall_no_pop_tokens_description = From fc5590a3fff2e980ca5b4e64c82fd63daae541c8 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Thu, 19 Jan 2023 20:08:39 +0100 Subject: [PATCH 021/121] Update ScannerInput.tsx --- fe1-web/src/core/components/ScannerInput.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fe1-web/src/core/components/ScannerInput.tsx b/fe1-web/src/core/components/ScannerInput.tsx index 7c196d69da..8b76beec74 100644 --- a/fe1-web/src/core/components/ScannerInput.tsx +++ b/fe1-web/src/core/components/ScannerInput.tsx @@ -2,10 +2,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import { StyleSheet, View } from 'react-native'; -import { Color, Spacing, Typography } from 'core/styles'; +import { Color, Spacing } from 'core/styles'; import AutocompleteInput from './AutocompleteInput'; -import { inputStyleSheet } from './Input'; import PoPButton from './PoPButton'; import PoPIcon from './PoPIcon'; @@ -34,17 +33,12 @@ const ScannerInput = ({ enabled, testID, }: IPropTypes) => { - const inputStyles = [Typography.paragraph, inputStyleSheet.input]; - - if (!enabled) { - inputStyles.push(inputStyleSheet.disabled); - } - return ( Date: Sun, 22 Jan 2023 15:55:43 +0100 Subject: [PATCH 022/121] added check for id in testlaochannel_catchup --- be1-go/channel/lao/mod_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/be1-go/channel/lao/mod_test.go b/be1-go/channel/lao/mod_test.go index 62a3bfeb34..86e5bcdbcb 100644 --- a/be1-go/channel/lao/mod_test.go +++ b/be1-go/channel/lao/mod_test.go @@ -207,6 +207,7 @@ func TestLAOChannel_Catchup(t *testing.T) { // Create the channel channel, err := NewChannel("channel0", fakeHub, messages[0], nolog, keypair.public, nil) require.NoError(t, err) + require.Equal(t, "0", messages[0]) laoChannel, ok := channel.(*Channel) require.True(t, ok) From 0a5d1910ec0d9d52ade8fd109dad942b3bc241ad Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Sun, 22 Jan 2023 15:58:19 +0100 Subject: [PATCH 023/121] fix error in check --- be1-go/channel/lao/mod_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be1-go/channel/lao/mod_test.go b/be1-go/channel/lao/mod_test.go index 86e5bcdbcb..5ae81a97e9 100644 --- a/be1-go/channel/lao/mod_test.go +++ b/be1-go/channel/lao/mod_test.go @@ -207,7 +207,7 @@ func TestLAOChannel_Catchup(t *testing.T) { // Create the channel channel, err := NewChannel("channel0", fakeHub, messages[0], nolog, keypair.public, nil) require.NoError(t, err) - require.Equal(t, "0", messages[0]) + require.Equal(t, "0", messages[0].MessageID) laoChannel, ok := channel.(*Channel) require.True(t, ok) From e2b6ee8da8126901fd91179205078ad485d1003d Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 22 Jan 2023 16:24:28 +0100 Subject: [PATCH 024/121] Address code review feedback Co-Authored-By: pierluca --- fe1-web/src/core/components/ScannerInput.tsx | 7 - .../__tests__/AutocompleteInput.test.tsx | 13 + .../__tests__/ScannerInput.test.tsx | 6 +- .../AutocompleteInput.test.tsx.snap | 50 +++ .../__snapshots__/ScannerInput.test.tsx.snap | 399 ++++++++++++++++-- 5 files changed, 434 insertions(+), 41 deletions(-) create mode 100644 fe1-web/src/core/components/__tests__/AutocompleteInput.test.tsx create mode 100644 fe1-web/src/core/components/__tests__/__snapshots__/AutocompleteInput.test.tsx.snap diff --git a/fe1-web/src/core/components/ScannerInput.tsx b/fe1-web/src/core/components/ScannerInput.tsx index 8b76beec74..005818cbcf 100644 --- a/fe1-web/src/core/components/ScannerInput.tsx +++ b/fe1-web/src/core/components/ScannerInput.tsx @@ -52,13 +52,6 @@ const ScannerInput = ({ testID={testID || undefined} /> - {/* */} diff --git a/fe1-web/src/core/components/__tests__/AutocompleteInput.test.tsx b/fe1-web/src/core/components/__tests__/AutocompleteInput.test.tsx new file mode 100644 index 0000000000..cbc5bcd097 --- /dev/null +++ b/fe1-web/src/core/components/__tests__/AutocompleteInput.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react-native'; +import React from 'react'; + +import AutocompleteInput from '../AutocompleteInput'; + +describe('AutocompleteInput', () => { + it('renders correctly', () => { + const { toJSON } = render( + {}} suggestions={['aaa', 'aab', 'aac']} />, + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/fe1-web/src/core/components/__tests__/ScannerInput.test.tsx b/fe1-web/src/core/components/__tests__/ScannerInput.test.tsx index cbc5bcd097..0ccd23420d 100644 --- a/fe1-web/src/core/components/__tests__/ScannerInput.test.tsx +++ b/fe1-web/src/core/components/__tests__/ScannerInput.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react-native'; import React from 'react'; -import AutocompleteInput from '../AutocompleteInput'; +import ScannerInput from '../ScannerInput'; -describe('AutocompleteInput', () => { +describe('ScannerInput', () => { it('renders correctly', () => { const { toJSON } = render( - {}} suggestions={['aaa', 'aab', 'aac']} />, + {}} suggestions={['aaa', 'aab', 'aac']} />, ); expect(toJSON()).toMatchSnapshot(); }); diff --git a/fe1-web/src/core/components/__tests__/__snapshots__/AutocompleteInput.test.tsx.snap b/fe1-web/src/core/components/__tests__/__snapshots__/AutocompleteInput.test.tsx.snap new file mode 100644 index 0000000000..ef348c7854 --- /dev/null +++ b/fe1-web/src/core/components/__tests__/__snapshots__/AutocompleteInput.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AutocompleteInput renders correctly 1`] = ` + + + + + + + +`; diff --git a/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap b/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap index ef348c7854..ad98d33ffb 100644 --- a/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap +++ b/fe1-web/src/core/components/__tests__/__snapshots__/ScannerInput.test.tsx.snap @@ -1,49 +1,386 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AutocompleteInput renders correctly 1`] = ` - +exports[`ScannerInput renders correctly 1`] = ` + + + + + + + + + + + + + + + + + aaa + + + + + + + + + + + aab + + + + + + + + + + + aac + + + + + + + + + + - + + "opacity": 1, + } + } + > + + + {"name":"ios-scan","size":25,"color":"#fff"} + + + + From 008cbf994bfe443391273ff26eae2618404ae258 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Sun, 22 Jan 2023 16:39:51 +0100 Subject: [PATCH 025/121] moved check to find error --- be1-go/channel/lao/mod_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/be1-go/channel/lao/mod_test.go b/be1-go/channel/lao/mod_test.go index 5ae81a97e9..dfcbd98154 100644 --- a/be1-go/channel/lao/mod_test.go +++ b/be1-go/channel/lao/mod_test.go @@ -207,11 +207,13 @@ func TestLAOChannel_Catchup(t *testing.T) { // Create the channel channel, err := NewChannel("channel0", fakeHub, messages[0], nolog, keypair.public, nil) require.NoError(t, err) - require.Equal(t, "0", messages[0].MessageID) laoChannel, ok := channel.(*Channel) require.True(t, ok) + _, ok = laoChannel.inbox.GetMessage(messages[0].MessageID) + require.True(t, ok) + time.Sleep(time.Millisecond) for i := 2; i < numMessages+1; i++ { From 3235b8f9743dd22b870b8a69fd97fd04390181f6 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:11:13 +0100 Subject: [PATCH 026/121] Add method that checks whether event is today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../dedis/popstellar/model/objects/event/Event.java | 10 ++++++++++ .../com/github/dedis/popstellar/utility/Constants.java | 3 +++ 2 files changed, 13 insertions(+) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java index f70a4ea792..c605a0eca6 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java @@ -1,5 +1,7 @@ package com.github.dedis.popstellar.model.objects.event; +import com.github.dedis.popstellar.utility.Constants; + /** Class modeling an Event */ public abstract class Event implements Comparable { @@ -28,4 +30,12 @@ public int compareTo(Event o) { return Long.compare(o.getEndTimestamp(), this.getEndTimestamp()); } + + /** + * @return true if event the event takes place today + */ + public boolean isEventToday() { + long currentTime = System.currentTimeMillis(); + return getStartTimestamp() - currentTime < Constants.MS_IN_A_DAY; + } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java index e3c452fa62..d6265c72b7 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java @@ -56,4 +56,7 @@ public class Constants { /** Standard size of the side of a displayed QR code */ public static final int QR_SIDE = 800; + + /** Number of milliseconds in a day */ + public static final long MS_IN_A_DAY = 86_400_000L; } From ad338968c328d50ee819df3d2abf629505f31196 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:12:20 +0100 Subject: [PATCH 027/121] Create adapters for future events, create abstract parent adapter for new adapter and existing one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../ui/detail/event/EventListAdapter.java | 110 +++----------- .../ui/detail/event/EventsAdapter.java | 143 ++++++++++++++++++ .../detail/event/UpcomingEventsAdapter.java | 60 ++++++++ 3 files changed, 220 insertions(+), 93 deletions(-) create mode 100644 fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java create mode 100644 fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java index 94757ebda7..03bf1a8187 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java @@ -1,43 +1,29 @@ package com.github.dedis.popstellar.ui.detail.event; import android.annotation.SuppressLint; -import android.util.Log; import android.view.*; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import androidx.cardview.widget.CardView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; import com.github.dedis.popstellar.R; -import com.github.dedis.popstellar.model.objects.Election; -import com.github.dedis.popstellar.model.objects.RollCall; -import com.github.dedis.popstellar.model.objects.event.*; -import com.github.dedis.popstellar.model.objects.security.PoPToken; -import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.model.objects.event.Event; +import com.github.dedis.popstellar.model.objects.event.EventCategory; import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; -import com.github.dedis.popstellar.ui.detail.event.election.fragments.ElectionFragment; -import com.github.dedis.popstellar.ui.detail.event.rollcall.RollCallFragment; -import com.github.dedis.popstellar.utility.error.ErrorUtils; -import com.github.dedis.popstellar.utility.error.UnknownLaoException; -import com.github.dedis.popstellar.utility.error.keys.KeyException; import java.util.*; -import java.util.stream.Collectors; import io.reactivex.Observable; import static com.github.dedis.popstellar.model.objects.event.EventCategory.*; -import static com.github.dedis.popstellar.ui.detail.LaoDetailActivity.setCurrentFragment; -public class EventListAdapter extends RecyclerView.Adapter { +public class EventListAdapter extends EventsAdapter { - private final LaoDetailViewModel viewModel; - private final FragmentActivity activity; private final EnumMap> eventsMap; private final boolean[] expanded = new boolean[3]; @@ -47,39 +33,34 @@ public class EventListAdapter extends RecyclerView.Adapter> events, FragmentActivity activity) { + super(events, viewModel, activity, TAG); this.eventsMap = new EnumMap<>(EventCategory.class); this.eventsMap.put(PAST, new ArrayList<>()); this.eventsMap.put(PRESENT, new ArrayList<>()); - this.eventsMap.put(FUTURE, new ArrayList<>()); - this.activity = activity; - this.viewModel = viewModel; expanded[PAST.ordinal()] = true; expanded[PRESENT.ordinal()] = true; - expanded[FUTURE.ordinal()] = true; - - subscribeToEventSet(events); - } - - private void subscribeToEventSet(Observable> observable) { - this.viewModel.addDisposable( - observable - .map(events -> events.stream().sorted().collect(Collectors.toList())) - // No need to check for error as the events errors already handles them - .subscribe(this::putEventsInMap, err -> Log.d(TAG, "ERROR", err))); } - /** A helper method that places the events in the correct key-value pair according to state */ + /** + * A helper method that places the events in the correct key-value pair according to state Closed + * events are put in the past section. Opened events in the current section. Created events that + * are to be open in less than 24 hours are also in the current section. Created events that are + * to be opened in more than 24 hours are not displayed here (They are displayed in a separate + * view) + */ @SuppressLint("NotifyDataSetChanged") - private void putEventsInMap(List events) { + @Override + public void updateEventSet(List events) { this.eventsMap.get(PAST).clear(); - this.eventsMap.get(FUTURE).clear(); this.eventsMap.get(PRESENT).clear(); for (Event event : events) { switch (event.getState()) { case CREATED: - eventsMap.get(FUTURE).add(event); + if (event.isEventToday()) { + eventsMap.get(PRESENT).add(event); + } break; case OPENED: eventsMap.get(PRESENT).add(event); @@ -160,50 +141,6 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi } } - /** - * Handle event content, that is setting icon based on RC/Election type, setting the name And - * setting an appropriate listener based on the type - */ - private void handleEventContent(EventViewHolder eventViewHolder, Event event) { - if (event.getType().equals(EventType.ELECTION)) { - eventViewHolder.eventIcon.setImageResource(R.drawable.ic_vote); - Election election = (Election) event; - eventViewHolder.eventTitle.setText(election.getName()); - View.OnClickListener listener = - view -> - LaoDetailActivity.setCurrentFragment( - activity.getSupportFragmentManager(), - R.id.fragment_election, - () -> ElectionFragment.newInstance(election.getId())); - eventViewHolder.eventCard.setOnClickListener(listener); - - } else if (event.getType().equals(EventType.ROLL_CALL)) { - eventViewHolder.eventIcon.setImageResource(R.drawable.ic_roll_call); - RollCall rollCall = (RollCall) event; - eventViewHolder.eventTitle.setText(rollCall.getName()); - eventViewHolder.eventCard.setOnClickListener( - view -> { - if (viewModel.isWalletSetup()) { - try { - PoPToken token = viewModel.getCurrentPopToken(rollCall); - setCurrentFragment( - activity.getSupportFragmentManager(), - R.id.fragment_roll_call, - () -> - RollCallFragment.newInstance( - token.getPublicKey(), rollCall.getPersistentId())); - } catch (KeyException e) { - ErrorUtils.logAndShow(activity, TAG, e, R.string.key_generation_exception); - } catch (UnknownLaoException e) { - ErrorUtils.logAndShow(activity, TAG, e, R.string.error_no_lao); - } - } else { - showWalletNotSetupWarning(); - } - }); - } - } - /** * Get the event at the indicated position. The computations are there to account for * expanded/collapsed sections and the fact that some elements are headers The caller must make @@ -307,25 +244,12 @@ public int getItemViewType(int position) { } public void showWalletNotSetupWarning() { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("You have to setup up your wallet before connecting."); builder.setPositiveButton("Ok", (dialog, which) -> dialog.dismiss()); builder.show(); } - public static class EventViewHolder extends RecyclerView.ViewHolder { - private final TextView eventTitle; - private final ImageView eventIcon; - private final CardView eventCard; - - public EventViewHolder(@NonNull View itemView) { - super(itemView); - eventTitle = itemView.findViewById(R.id.event_card_text_view); - eventIcon = itemView.findViewById(R.id.event_type_image); - eventCard = itemView.findViewById(R.id.event_card_view); - } - } - public static class HeaderViewHolder extends RecyclerView.ViewHolder { private final TextView headerTitle; private final TextView headerNumber; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java new file mode 100644 index 0000000000..bd3d83f633 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java @@ -0,0 +1,143 @@ +package com.github.dedis.popstellar.ui.detail.event; + +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.cardview.widget.CardView; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.dedis.popstellar.R; +import com.github.dedis.popstellar.model.objects.Election; +import com.github.dedis.popstellar.model.objects.RollCall; +import com.github.dedis.popstellar.model.objects.event.Event; +import com.github.dedis.popstellar.model.objects.event.EventType; +import com.github.dedis.popstellar.model.objects.security.PoPToken; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.election.fragments.ElectionFragment; +import com.github.dedis.popstellar.ui.detail.event.rollcall.RollCallFragment; +import com.github.dedis.popstellar.utility.error.ErrorUtils; +import com.github.dedis.popstellar.utility.error.UnknownLaoException; +import com.github.dedis.popstellar.utility.error.keys.KeyException; + +import java.util.*; +import java.util.stream.Collectors; + +import io.reactivex.Observable; + +import static com.github.dedis.popstellar.ui.detail.LaoDetailActivity.setCurrentFragment; + +public abstract class EventsAdapter extends RecyclerView.Adapter { + private List events; + private final LaoDetailViewModel viewModel; + private final FragmentActivity activity; + private final String TAG; + + public EventsAdapter( + Observable> observable, + LaoDetailViewModel viewModel, + FragmentActivity activity, + String TAG) { + this.events = new ArrayList<>(events); + this.viewModel = viewModel; + this.activity = activity; + this.TAG = TAG; + subscribeToEventSet(observable); + } + + private void subscribeToEventSet(Observable> observable) { + this.viewModel.addDisposable( + observable + .map(events -> events.stream().sorted().collect(Collectors.toList())) + // No need to check for error as the events errors already handles them + .subscribe(this::updateEventSet, err -> Log.e(TAG, "ERROR", err))); + } + + public abstract void updateEventSet(List events); + + public List getEvents() { + return new ArrayList<>(events); + } + + public void setEvents(List events) { + this.events = new ArrayList<>(events); + } + + public LaoDetailViewModel getViewModel() { + return viewModel; + } + + public FragmentActivity getActivity() { + return activity; + } + + /** + * Handle event content, that is setting icon based on RC/Election type, setting the name And + * setting an appropriate listener based on the type + */ + protected void handleEventContent(EventViewHolder eventViewHolder, Event event) { + if (event.getType().equals(EventType.ELECTION)) { + eventViewHolder.eventIcon.setImageResource(R.drawable.ic_vote); + Election election = (Election) event; + eventViewHolder.eventTitle.setText(election.getName()); + View.OnClickListener listener = + view -> + LaoDetailActivity.setCurrentFragment( + activity.getSupportFragmentManager(), + R.id.fragment_election, + () -> ElectionFragment.newInstance(election.getId())); + eventViewHolder.eventCard.setOnClickListener(listener); + + } else if (event.getType().equals(EventType.ROLL_CALL)) { + eventViewHolder.eventIcon.setImageResource(R.drawable.ic_roll_call); + RollCall rollCall = (RollCall) event; + eventViewHolder.eventTitle.setText(rollCall.getName()); + eventViewHolder.eventCard.setOnClickListener( + view -> { + if (viewModel.isWalletSetup()) { + try { + PoPToken token = viewModel.getCurrentPopToken(rollCall); + setCurrentFragment( + activity.getSupportFragmentManager(), + R.id.fragment_roll_call, + () -> + RollCallFragment.newInstance( + token.getPublicKey(), rollCall.getPersistentId())); + } catch (KeyException e) { + ErrorUtils.logAndShow(activity, TAG, e, R.string.key_generation_exception); + } catch (UnknownLaoException e) { + ErrorUtils.logAndShow(activity, TAG, e, R.string.error_no_lao); + } + } else { + showWalletNotSetupWarning(); + } + }); + eventViewHolder.eventTitle.setText(rollCall.getName()); + } + } + + public void showWalletNotSetupWarning() { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("You have to setup up your wallet before connecting."); + builder.setPositiveButton("Ok", (dialog, which) -> dialog.dismiss()); + builder.show(); + } + + public static class EventViewHolder extends RecyclerView.ViewHolder { + private final TextView eventTitle; + private final ImageView eventIcon; + private final CardView eventCard; + + public EventViewHolder(@NonNull View itemView) { + super(itemView); + eventTitle = itemView.findViewById(R.id.event_card_text_view); + eventIcon = itemView.findViewById(R.id.event_type_image); + eventCard = itemView.findViewById(R.id.event_card_view); + } + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java new file mode 100644 index 0000000000..118a4071d0 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java @@ -0,0 +1,60 @@ +package com.github.dedis.popstellar.ui.detail.event; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.dedis.popstellar.R; +import com.github.dedis.popstellar.model.objects.event.Event; +import com.github.dedis.popstellar.model.objects.event.EventState; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.reactivex.Observable; + +public class UpcomingEventsAdapter extends EventsAdapter { + + public UpcomingEventsAdapter( + Observable> observable, + LaoDetailViewModel viewModel, + FragmentActivity activity, + String TAG) { + super(observable, viewModel, activity, TAG); + } + + @SuppressLint("NotifyDataSetChanged") + @Override + public void updateEventSet(List events) { + this.setEvents( + events.stream() + .filter(event -> event.getState().equals(EventState.CREATED) && !event.isEventToday()) + .collect(Collectors.toList())); + notifyDataSetChanged(); + } + + @NonNull + @Override + public EventViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new EventViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.event_layout, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + EventViewHolder eventViewHolder = (EventViewHolder) holder; + Event event = this.getEvents().get(position); + this.handleEventContent(eventViewHolder, event); + } + + @Override + public int getItemCount() { + return getEvents().size(); + } +} From 1005c5debc1d349f928180aca19a2dbd346f5ec8 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:16:37 +0100 Subject: [PATCH 028/121] Remove useless interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../dedis/popstellar/ui/detail/event/AddEventListener.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/AddEventListener.java diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/AddEventListener.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/AddEventListener.java deleted file mode 100644 index 1c90666661..0000000000 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/AddEventListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.dedis.popstellar.ui.detail.event; - -public interface AddEventListener { - - void addEvent(); -} From f900bfd647ab9c8bcbe4c8d94590371a7f3a54b2 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:31:59 +0100 Subject: [PATCH 029/121] Create event list package, move relevant Classes there and rename laodetailfragment to a more appropriated EventListFragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../popstellar/ui/detail/LaoDetailActivity.java | 3 ++- .../election/fragments/CastVoteFragment.java | 6 ++++-- .../fragments/ElectionSetupFragment.java | 6 ++++-- .../event/{ => eventlist}/EventListAdapter.java | 3 ++- .../event/{ => eventlist}/EventListDivider.java | 2 +- .../eventlist/EventListFragment.java} | 16 +++++++++------- .../event/{ => eventlist}/EventsAdapter.java | 3 +-- .../{ => eventlist}/UpcomingEventsAdapter.java | 2 +- .../event/rollcall/RollCallCreationFragment.java | 6 ++++-- .../detail/event/rollcall/RollCallFragment.java | 6 ++++-- .../ui/qrcode/QRCodeScanningFragment.java | 6 ++++-- .../ui/detail/EventListAdapterTest.java | 2 +- ...gmentTest.java => EventListFragmentTest.java} | 4 +--- 13 files changed, 38 insertions(+), 27 deletions(-) rename fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/{ => eventlist}/EventListAdapter.java (98%) rename fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/{ => eventlist}/EventListDivider.java (96%) rename fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/{LaoDetailFragment.java => event/eventlist/EventListFragment.java} (90%) rename fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/{ => eventlist}/EventsAdapter.java (98%) rename fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/{ => eventlist}/UpcomingEventsAdapter.java (96%) rename fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/{LaoDetailFragmentTest.java => EventListFragmentTest.java} (98%) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java index 53e2ad7afd..066786f1a5 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java @@ -14,6 +14,7 @@ import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.databinding.LaoDetailActivityBinding; import com.github.dedis.popstellar.repository.remote.GlobalNetworkManager; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.ui.detail.token.TokenListFragment; import com.github.dedis.popstellar.ui.detail.witness.WitnessingFragment; import com.github.dedis.popstellar.ui.digitalcash.DigitalCashActivity; @@ -123,7 +124,7 @@ private void openInviteTab() { private void openEventsTab() { setCurrentFragment( - getSupportFragmentManager(), R.id.fragment_lao_detail, LaoDetailFragment::newInstance); + getSupportFragmentManager(), R.id.fragment_lao_detail, EventListFragment::newInstance); } private void openTokensTab() { diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/CastVoteFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/CastVoteFragment.java index 58c891f72e..3c2172202f 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/CastVoteFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/CastVoteFragment.java @@ -15,9 +15,11 @@ import com.github.dedis.popstellar.model.objects.Election; import com.github.dedis.popstellar.model.objects.view.LaoView; import com.github.dedis.popstellar.repository.ElectionRepository; -import com.github.dedis.popstellar.ui.detail.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.election.ZoomOutTransformer; import com.github.dedis.popstellar.ui.detail.event.election.adapters.CastVoteViewPagerAdapter; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.utility.error.UnknownElectionException; import com.github.dedis.popstellar.utility.error.UnknownLaoException; @@ -160,7 +162,7 @@ private void castVote(View voteButton) { setCurrentFragment( getParentFragmentManager(), R.id.fragment_lao_detail, - LaoDetailFragment::newInstance); + EventListFragment::newInstance); // Toast ? + send back to election screen or details screen ? Toast.makeText(requireContext(), "vote successfully sent !", Toast.LENGTH_LONG) .show(); diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/ElectionSetupFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/ElectionSetupFragment.java index f48ca2e60f..85159f1803 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/ElectionSetupFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/election/fragments/ElectionSetupFragment.java @@ -16,10 +16,12 @@ import com.github.dedis.popstellar.databinding.ElectionSetupFragmentBinding; import com.github.dedis.popstellar.model.network.method.message.data.election.ElectionQuestion.Question; import com.github.dedis.popstellar.model.network.method.message.data.election.ElectionVersion; -import com.github.dedis.popstellar.ui.detail.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.AbstractEventCreationFragment; import com.github.dedis.popstellar.ui.detail.event.election.ZoomOutTransformer; import com.github.dedis.popstellar.ui.detail.event.election.adapters.ElectionSetupViewPagerAdapter; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.utility.error.ErrorUtils; import java.util.ArrayList; @@ -252,7 +254,7 @@ private void setupElectionSubmitButton() { setCurrentFragment( getParentFragmentManager(), R.id.fragment_lao_detail, - LaoDetailFragment::newInstance), + EventListFragment::newInstance), error -> ErrorUtils.logAndShow( requireContext(), TAG, error, R.string.error_create_election))); diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java similarity index 98% rename from fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java rename to fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java index 03bf1a8187..17ba5116d7 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail.event; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.annotation.SuppressLint; import android.view.*; @@ -15,6 +15,7 @@ import com.github.dedis.popstellar.model.objects.event.Event; import com.github.dedis.popstellar.model.objects.event.EventCategory; import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; import java.util.*; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListDivider.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java similarity index 96% rename from fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListDivider.java rename to fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java index 105faf019f..346f8be3bd 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventListDivider.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail.event; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.content.Context; import android.graphics.*; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java similarity index 90% rename from fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailFragment.java rename to fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index 137e556804..3283c7eb7e 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.os.Bundle; import android.util.Log; @@ -15,7 +15,9 @@ import com.github.dedis.popstellar.databinding.LaoDetailFragmentBinding; import com.github.dedis.popstellar.model.Role; import com.github.dedis.popstellar.model.objects.event.EventType; -import com.github.dedis.popstellar.ui.detail.event.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; import com.github.dedis.popstellar.ui.detail.event.election.fragments.ElectionSetupFragment; import com.github.dedis.popstellar.ui.detail.event.rollcall.RollCallCreationFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -25,11 +27,11 @@ import dagger.hilt.android.AndroidEntryPoint; -/** Fragment used to display the LAO Detail UI */ +/** Fragment used to display the list of events */ @AndroidEntryPoint -public class LaoDetailFragment extends Fragment { +public class EventListFragment extends Fragment { - public static final String TAG = LaoDetailFragment.class.getSimpleName(); + public static final String TAG = EventListFragment.class.getSimpleName(); @Inject Gson gson; @@ -37,8 +39,8 @@ public class LaoDetailFragment extends Fragment { private LaoDetailViewModel viewModel; private boolean isRotated = false; - public static LaoDetailFragment newInstance() { - return new LaoDetailFragment(); + public static EventListFragment newInstance() { + return new EventListFragment(); } @Nullable diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java similarity index 98% rename from fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java rename to fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index bd3d83f633..f2b1dbacca 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail.event; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.util.Log; import android.view.View; @@ -43,7 +43,6 @@ public EventsAdapter( LaoDetailViewModel viewModel, FragmentActivity activity, String TAG) { - this.events = new ArrayList<>(events); this.viewModel = viewModel; this.activity = activity; this.TAG = TAG; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java similarity index 96% rename from fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java rename to fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java index 118a4071d0..7280f55404 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail.event; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.annotation.SuppressLint; import android.view.LayoutInflater; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallCreationFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallCreationFragment.java index 3065f26b95..2d4af5167a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallCreationFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallCreationFragment.java @@ -12,8 +12,10 @@ import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.databinding.RollCallCreateFragmentBinding; -import com.github.dedis.popstellar.ui.detail.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.AbstractEventCreationFragment; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.ui.qrcode.QRCodeScanningFragment; import com.github.dedis.popstellar.utility.error.ErrorUtils; @@ -158,7 +160,7 @@ private void createRollCall(boolean open) { setCurrentFragment( getParentFragmentManager(), R.id.fragment_lao_detail, - LaoDetailFragment::newInstance), + EventListFragment::newInstance), error -> ErrorUtils.logAndShow( requireContext(), TAG, error, R.string.error_create_rollcall))); diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallFragment.java index b1a105aec4..ef6065e14f 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/rollcall/RollCallFragment.java @@ -19,7 +19,9 @@ import com.github.dedis.popstellar.model.objects.event.EventState; import com.github.dedis.popstellar.model.objects.security.PublicKey; import com.github.dedis.popstellar.model.qrcode.PopTokenData; -import com.github.dedis.popstellar.ui.detail.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.ui.qrcode.QRCodeScanningFragment; import com.github.dedis.popstellar.utility.Constants; import com.github.dedis.popstellar.utility.error.ErrorUtils; @@ -118,7 +120,7 @@ public View onCreateView( setCurrentFragment( getParentFragmentManager(), R.id.fragment_lao_detail, - LaoDetailFragment::newInstance), + EventListFragment::newInstance), error -> ErrorUtils.logAndShow( requireContext(), TAG, error, R.string.error_close_rollcall))); diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QRCodeScanningFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QRCodeScanningFragment.java index 52a5134b40..fb682d685b 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QRCodeScanningFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/qrcode/QRCodeScanningFragment.java @@ -17,7 +17,9 @@ import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.databinding.QrcodeFragmentBinding; -import com.github.dedis.popstellar.ui.detail.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListFragment; import com.github.dedis.popstellar.ui.home.HomeActivity; import com.github.dedis.popstellar.ui.home.HomeViewModel; import com.github.dedis.popstellar.utility.error.ErrorUtils; @@ -226,7 +228,7 @@ private void setupClickCloseListener() { setCurrentFragment( getParentFragmentManager(), R.id.fragment_lao_detail, - LaoDetailFragment::newInstance), + EventListFragment::newInstance), error -> ErrorUtils.logAndShow( requireContext(), TAG, error, R.string.error_close_rollcall)))); diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java index ee4eeacb68..aef00bc387 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java @@ -7,7 +7,7 @@ import com.github.dedis.popstellar.model.objects.event.Event; import com.github.dedis.popstellar.model.objects.event.EventState; import com.github.dedis.popstellar.testutils.*; -import com.github.dedis.popstellar.ui.detail.event.EventListAdapter; +import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListAdapter; import org.junit.Rule; import org.junit.Test; diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/LaoDetailFragmentTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java similarity index 98% rename from fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/LaoDetailFragmentTest.java rename to fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java index 49adedc470..81bb8c2f5f 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/LaoDetailFragmentTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java @@ -18,8 +18,6 @@ import com.github.dedis.popstellar.model.objects.Channel; import com.github.dedis.popstellar.model.objects.Lao; import com.github.dedis.popstellar.model.objects.security.*; -import com.github.dedis.popstellar.model.objects.view.LaoView; -import com.github.dedis.popstellar.model.qrcode.ConnectToLao; import com.github.dedis.popstellar.repository.LAORepository; import com.github.dedis.popstellar.repository.remote.GlobalNetworkManager; import com.github.dedis.popstellar.repository.remote.MessageSender; @@ -62,7 +60,7 @@ @LargeTest @HiltAndroidTest @RunWith(AndroidJUnit4.class) -public class LaoDetailFragmentTest { +public class EventListFragmentTest { private static final String LAO_NAME = "LAO"; private static final KeyPair KEY_PAIR = Base64DataUtils.generateKeyPair(); From ef42da5e3fc9aa73e46b6203c44ab64d441ddb54 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:41:29 +0100 Subject: [PATCH 030/121] Rename res file to match fragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../layout/{lao_detail_fragment.xml => event_list_fragment.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fe2-android/app/src/main/res/layout/{lao_detail_fragment.xml => event_list_fragment.xml} (100%) diff --git a/fe2-android/app/src/main/res/layout/lao_detail_fragment.xml b/fe2-android/app/src/main/res/layout/event_list_fragment.xml similarity index 100% rename from fe2-android/app/src/main/res/layout/lao_detail_fragment.xml rename to fe2-android/app/src/main/res/layout/event_list_fragment.xml From 93b2453dd0d66c8183f349a5f423746799fc4bcd Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:42:01 +0100 Subject: [PATCH 031/121] Fixe binding following renaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../ui/detail/event/eventlist/EventListFragment.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index 3283c7eb7e..531326563a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -12,7 +12,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.github.dedis.popstellar.R; -import com.github.dedis.popstellar.databinding.LaoDetailFragmentBinding; +import com.github.dedis.popstellar.databinding.EventListFragmentBinding; import com.github.dedis.popstellar.model.Role; import com.github.dedis.popstellar.model.objects.event.EventType; import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; @@ -35,7 +35,7 @@ public class EventListFragment extends Fragment { @Inject Gson gson; - private LaoDetailFragmentBinding binding; + private EventListFragmentBinding binding; private LaoDetailViewModel viewModel; private boolean isRotated = false; @@ -49,7 +49,7 @@ public View onCreateView( @NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = LaoDetailFragmentBinding.inflate(inflater, container, false); + binding = EventListFragmentBinding.inflate(inflater, container, false); viewModel = LaoDetailActivity.obtainViewModel(requireActivity()); binding.setLifecycleOwner(requireActivity()); From 6216a6262cdd8410dfb5ace2dbbb68fa710df7c9 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 21:42:32 +0100 Subject: [PATCH 032/121] Fix event list adapter following removal of future section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListAdapter.java | 43 ++++--------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java index 17ba5116d7..8229554ca6 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java @@ -1,6 +1,7 @@ package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.annotation.SuppressLint; +import android.util.Log; import android.view.*; import android.widget.ImageView; import android.widget.TextView; @@ -21,13 +22,14 @@ import io.reactivex.Observable; -import static com.github.dedis.popstellar.model.objects.event.EventCategory.*; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PAST; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PRESENT; public class EventListAdapter extends EventsAdapter { private final EnumMap> eventsMap; - private final boolean[] expanded = new boolean[3]; + private final boolean[] expanded = new boolean[2]; public static final int TYPE_HEADER = 0; public static final int TYPE_EVENT = 1; public static final String TAG = EventListAdapter.class.getSimpleName(); @@ -108,9 +110,6 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi case PRESENT: headerTitle.setText(R.string.present_header_title); break; - case FUTURE: - headerTitle.setText(R.string.future_header_title); - break; case PAST: headerTitle.setText(R.string.past_header_title); break; @@ -148,7 +147,6 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi * sure the position is occupied by an event or this will throw an exception */ private Event getEvent(int position) { - int nbrOfFutureEvents = Objects.requireNonNull(eventsMap.get(FUTURE)).size(); int nbrOfPresentEvents = Objects.requireNonNull(eventsMap.get(PRESENT)).size(); int nbrOfPastEvents = Objects.requireNonNull(eventsMap.get(PAST)).size(); @@ -160,14 +158,8 @@ private Event getEvent(int position) { } eventAccumulator += nbrOfPresentEvents; } - if (expanded[FUTURE.ordinal()]) { - if (position <= nbrOfFutureEvents + eventAccumulator + 1) { - return Objects.requireNonNull(eventsMap.get(FUTURE)).get(position - eventAccumulator - 2); - } - eventAccumulator += nbrOfFutureEvents; - } - if (expanded[PAST.ordinal()] && position <= nbrOfPastEvents + eventAccumulator + 2) { - return Objects.requireNonNull(eventsMap.get(PAST)).get(position - eventAccumulator - 3); + if (expanded[PAST.ordinal()] && position <= nbrOfPastEvents + eventAccumulator + 1) { + return Objects.requireNonNull(eventsMap.get(PAST)).get(position - eventAccumulator - 2); } throw new IllegalStateException("no event matches"); } @@ -178,9 +170,7 @@ private Event getEvent(int position) { * sure the position is occupied by a header or this will throw an exception */ private EventCategory getHeaderCategory(int position) { - int nbrOfFutureEvents = Objects.requireNonNull(eventsMap.get(FUTURE)).size(); int nbrOfPresentEvents = Objects.requireNonNull(eventsMap.get(PRESENT)).size(); - if (position == 0) { return PRESENT; } @@ -188,22 +178,15 @@ private EventCategory getHeaderCategory(int position) { if (expanded[PRESENT.ordinal()]) { eventAccumulator += nbrOfPresentEvents; } - if (position == eventAccumulator + 1) { - return FUTURE; - } - if (expanded[FUTURE.ordinal()]) { - eventAccumulator += nbrOfFutureEvents; - } - if (position == eventAccumulator + 2) { return PAST; } + Log.e(TAG, "Illegal position " + position); throw new IllegalStateException("No event category"); } @Override public int getItemCount() { - int nbrOfFutureEvents = eventsMap.get(FUTURE).size(); int nbrOfPresentEvents = eventsMap.get(PRESENT).size(); int nbrOfPastEvents = eventsMap.get(PAST).size(); int eventAccumulator = 0; @@ -211,18 +194,14 @@ public int getItemCount() { if (expanded[PRESENT.ordinal()]) { eventAccumulator = nbrOfPresentEvents; } - if (expanded[FUTURE.ordinal()]) { - eventAccumulator += nbrOfFutureEvents; - } if (expanded[PAST.ordinal()]) { eventAccumulator += nbrOfPastEvents; } - return eventAccumulator + 3; // The number expanded of events + the 3 sub-headers + return eventAccumulator + 2; // The number expanded of events + the 2 sub-headers } @Override public int getItemViewType(int position) { - int nbrOfFutureEvents = eventsMap.get(FUTURE).size(); int nbrOfPresentEvents = eventsMap.get(PRESENT).size(); int eventAccumulator = 0; @@ -235,12 +214,6 @@ public int getItemViewType(int position) { if (position == eventAccumulator + 1) { return TYPE_HEADER; } - if (expanded[FUTURE.ordinal()]) { - eventAccumulator += nbrOfFutureEvents; - } - if (position == eventAccumulator + 2) { - return TYPE_HEADER; - } return TYPE_EVENT; } From b01730e35ad221219952763329be152e780bb5b8 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 22:52:36 +0100 Subject: [PATCH 033/121] Add upcoming events card to layout and adapt event list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../main/res/layout/event_header_layout.xml | 7 +-- .../app/src/main/res/layout/event_layout.xml | 7 ++- .../main/res/layout/event_list_fragment.xml | 60 +++++++++++++++---- .../app/src/main/res/values/dimens.xml | 9 ++- .../app/src/main/res/values/strings.xml | 6 +- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/fe2-android/app/src/main/res/layout/event_header_layout.xml b/fe2-android/app/src/main/res/layout/event_header_layout.xml index 51edd1ae06..d351d1d453 100644 --- a/fe2-android/app/src/main/res/layout/event_header_layout.xml +++ b/fe2-android/app/src/main/res/layout/event_header_layout.xml @@ -9,12 +9,10 @@ android:id="@+id/event_header_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/header_layout_margin_start" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - android:textColor="@color/black" - android:textStyle="bold" + style="@style/text_high_emphasis" android:textSize="@dimen/header_title_text_size" /> diff --git a/fe2-android/app/src/main/res/layout/event_layout.xml b/fe2-android/app/src/main/res/layout/event_layout.xml index fa36986169..31c5a5b493 100644 --- a/fe2-android/app/src/main/res/layout/event_layout.xml +++ b/fe2-android/app/src/main/res/layout/event_layout.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + app:layout_constraintTop_toTopOf="parent" + app:cardCornerRadius="@dimen/corner_radius_small"> - + diff --git a/fe2-android/app/src/main/res/layout/event_list_fragment.xml b/fe2-android/app/src/main/res/layout/event_list_fragment.xml index 3fc988f2b5..7b713b7991 100644 --- a/fe2-android/app/src/main/res/layout/event_list_fragment.xml +++ b/fe2-android/app/src/main/res/layout/event_list_fragment.xml @@ -4,28 +4,65 @@ - + android:layout_height="wrap_content" + android:elevation="0dp" + android:clickable="true" + android:layout_marginTop="@dimen/container_margin" + app:cardCornerRadius="@dimen/corner_radius_small"> - + android:layout_height="wrap_content" + android:paddingVertical="@dimen/padding_big"> + + + + + + + - + + + - diff --git a/fe2-android/app/src/main/res/values/dimens.xml b/fe2-android/app/src/main/res/values/dimens.xml index 17bc151d33..514e573d5e 100644 --- a/fe2-android/app/src/main/res/values/dimens.xml +++ b/fe2-android/app/src/main/res/values/dimens.xml @@ -63,14 +63,15 @@ 8dp 20dp 30dp - 24dp + 8dp 55dp 16dp 8dp - 14sp + 16sp 45dp + 18sp 8dp @@ -94,9 +95,10 @@ 20dp - + 18sp 8dp + 16dp 28dp @@ -139,6 +141,7 @@ 16dp 8dp + 14dp 12dp 5dp 1dp diff --git a/fe2-android/app/src/main/res/values/strings.xml b/fe2-android/app/src/main/res/values/strings.xml index 6b5ca691e8..8445e00d59 100644 --- a/fe2-android/app/src/main/res/values/strings.xml +++ b/fe2-android/app/src/main/res/values/strings.xml @@ -44,9 +44,9 @@ New Election New Roll Call - Current - Upcoming - Previous + Current Events + Upcoming Events + Past Events Connected to server Identifier Name From 9566f250a265117f8ea4ae3ab92de55ff90f364a Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 23:09:34 +0100 Subject: [PATCH 034/121] Fix wrong time format use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../com/github/dedis/popstellar/model/objects/event/Event.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java index c605a0eca6..068474e134 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java @@ -36,6 +36,6 @@ public int compareTo(Event o) { */ public boolean isEventToday() { long currentTime = System.currentTimeMillis(); - return getStartTimestamp() - currentTime < Constants.MS_IN_A_DAY; + return getEndTimestampInMillis() - currentTime < Constants.MS_IN_A_DAY; } } From 2505ed4ade35fd6a856fcbcdd9259cafef266d37 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 23:10:18 +0100 Subject: [PATCH 035/121] Make upcoming events card disappear if no future event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListFragment.java | 20 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 1 + 2 files changed, 21 insertions(+) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index 531326563a..40a07c68e9 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -14,12 +14,14 @@ import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.databinding.EventListFragmentBinding; import com.github.dedis.popstellar.model.Role; +import com.github.dedis.popstellar.model.objects.event.EventState; import com.github.dedis.popstellar.model.objects.event.EventType; import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; import com.github.dedis.popstellar.ui.detail.event.election.fragments.ElectionSetupFragment; import com.github.dedis.popstellar.ui.detail.event.rollcall.RollCallCreationFragment; +import com.github.dedis.popstellar.utility.error.ErrorUtils; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.gson.Gson; @@ -68,6 +70,24 @@ public View onCreateView( binding.addRollCall.setOnClickListener(openCreateEvent(EventType.ROLL_CALL)); binding.addRollCallText.setOnClickListener(openCreateEvent(EventType.ROLL_CALL)); + // Observing events so that we know when to display the upcoming events card + viewModel.addDisposable( + viewModel + .getEvents() + .subscribe( + events -> + binding.upcomingEventsCard.setVisibility( + events.stream() + .anyMatch( // We are looking for any event that is in future section + event -> + // We want created events that are in more than 24 hours + event.getState().equals(EventState.CREATED) + && !event.isEventToday()) + ? View.VISIBLE + : View.GONE), + error -> + ErrorUtils.logAndShow(requireContext(), TAG, R.string.error_event_observed))); + return binding.getRoot(); } diff --git a/fe2-android/app/src/main/res/values/strings.xml b/fe2-android/app/src/main/res/values/strings.xml index 8445e00d59..28c8fad85d 100644 --- a/fe2-android/app/src/main/res/values/strings.xml +++ b/fe2-android/app/src/main/res/values/strings.xml @@ -263,6 +263,7 @@ An error occurred during the storage of the wallet seed Error, this QR code is not a pop token or the key format is invalid Error, this QR code is not a main public key or the key format is invalid + Error, updating events unsuccessful An error response was replied by the server\nError %1$d - %2$s From 5d032d22a4e66ca38dd7d56a31cb56ad0707dad3 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 23:25:10 +0100 Subject: [PATCH 036/121] Improve layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../ui/detail/event/eventlist/EventListFragment.java | 2 -- fe2-android/app/src/main/res/layout/event_header_layout.xml | 1 + fe2-android/app/src/main/res/layout/event_layout.xml | 1 + fe2-android/app/src/main/res/layout/event_list_fragment.xml | 3 +-- fe2-android/app/src/main/res/values/dimens.xml | 1 + 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index 40a07c68e9..a46532f86a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -153,8 +153,6 @@ private void setupEventListAdapter() { LinearLayoutManager mLayoutManager = new LinearLayoutManager(getContext()); eventList.setLayoutManager(mLayoutManager); - EventListDivider divider = new EventListDivider(getContext()); - eventList.addItemDecoration(divider); eventList.setAdapter(eventListAdapter); } } diff --git a/fe2-android/app/src/main/res/layout/event_header_layout.xml b/fe2-android/app/src/main/res/layout/event_header_layout.xml index d351d1d453..380825012f 100644 --- a/fe2-android/app/src/main/res/layout/event_header_layout.xml +++ b/fe2-android/app/src/main/res/layout/event_header_layout.xml @@ -3,6 +3,7 @@ android:id="@+id/header_layout" android:layout_width="match_parent" android:layout_height="@dimen/header_card_view_height" + android:layout_marginTop="@dimen/event_list_sections_margin" xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/fe2-android/app/src/main/res/values/dimens.xml b/fe2-android/app/src/main/res/values/dimens.xml index 514e573d5e..579ee9d7c1 100644 --- a/fe2-android/app/src/main/res/values/dimens.xml +++ b/fe2-android/app/src/main/res/values/dimens.xml @@ -99,6 +99,7 @@ 18sp 8dp 16dp + 2dp 28dp From c76f77de637067c1f8c99796ea061606fc6c9470 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 23:26:02 +0100 Subject: [PATCH 037/121] Remove no longer used class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListDivider.java | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java deleted file mode 100644 index 346f8be3bd..0000000000 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListDivider.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.dedis.popstellar.ui.detail.event.eventlist; - -import android.content.Context; -import android.graphics.*; -import android.util.TypedValue; -import android.view.View; - -import androidx.recyclerview.widget.RecyclerView; - -/** - * The purpose of this class is to provide a custom separator between elements of the event list In - * particular we want only separators between sections and not between events or at the end - */ -public class EventListDivider extends RecyclerView.ItemDecoration { - public static final String TAG = EventListDivider.class.getSimpleName(); - private final Paint mPaint; - private final int mHeightDp; - - public EventListDivider(Context context) { - this(context, Color.argb((int) (255 * 0.2), 0, 0, 0), 1f); - } - - public EventListDivider(Context context, int color, float heightDp) { - mPaint = new Paint(); - mPaint.setStyle(Paint.Style.FILL); - mPaint.setColor(color); - mHeightDp = - (int) - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, heightDp, context.getResources().getDisplayMetrics()); - } - - private boolean hasDividerOnBottom(View view, RecyclerView parent, RecyclerView.State state) { - int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition(); - return position < state.getItemCount() - && parent.getAdapter().getItemViewType(position + 1) == EventListAdapter.TYPE_HEADER; - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - for (int i = 0; i < parent.getChildCount(); i++) { - View view = parent.getChildAt(i); - if (hasDividerOnBottom(view, parent, state)) { - c.drawRect( - view.getLeft(), - view.getBottom(), - view.getRight(), - view.getBottom() + (float) mHeightDp, - mPaint); - } - } - } -} From 47da03a875ee5d78789a0fd8b0d2f58e38a07d9a Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Tue, 24 Jan 2023 23:55:06 +0100 Subject: [PATCH 038/121] Add upcoming events fragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../detail/event/UpcomingEventsFragment.java | 50 +++++++++++++++++++ .../event/eventlist/EventListFragment.java | 9 ++++ .../detail/event/eventlist/EventsAdapter.java | 3 ++ .../main/res/layout/event_header_layout.xml | 2 +- .../res/layout/upcoming_events_fragment.xml | 21 ++++++++ .../app/src/main/res/values/dimens.xml | 1 + 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsFragment.java create mode 100644 fe2-android/app/src/main/res/layout/upcoming_events_fragment.xml diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsFragment.java new file mode 100644 index 0000000000..c5d8c721e4 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/UpcomingEventsFragment.java @@ -0,0 +1,50 @@ +package com.github.dedis.popstellar.ui.detail.event; + +import android.os.Bundle; +import android.view.*; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.github.dedis.popstellar.R; +import com.github.dedis.popstellar.databinding.UpcomingEventsFragmentBinding; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; +import com.github.dedis.popstellar.ui.detail.event.eventlist.UpcomingEventsAdapter; + +public class UpcomingEventsFragment extends Fragment { + + private static final String TAG = UpcomingEventsFragment.class.getSimpleName(); + private LaoDetailViewModel viewModel; + + public static UpcomingEventsFragment newInstance() { + return new UpcomingEventsFragment(); + } + + @Nullable + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + UpcomingEventsFragmentBinding binding = + UpcomingEventsFragmentBinding.inflate(inflater, container, false); + viewModel = LaoDetailActivity.obtainViewModel(requireActivity()); + + binding.upcomingEventsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + binding.upcomingEventsRecyclerView.setAdapter( + new UpcomingEventsAdapter(viewModel.getEvents(), viewModel, requireActivity(), TAG)); + + return binding.getRoot(); + } + + @Override + public void onResume() { + super.onResume(); + viewModel.setPageTitle(R.string.future_header_title); + viewModel.setIsTab(false); + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index a46532f86a..830f213133 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -19,6 +19,7 @@ import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; +import com.github.dedis.popstellar.ui.detail.event.UpcomingEventsFragment; import com.github.dedis.popstellar.ui.detail.event.election.fragments.ElectionSetupFragment; import com.github.dedis.popstellar.ui.detail.event.rollcall.RollCallCreationFragment; import com.github.dedis.popstellar.utility.error.ErrorUtils; @@ -88,6 +89,14 @@ public View onCreateView( error -> ErrorUtils.logAndShow(requireContext(), TAG, R.string.error_event_observed))); + // Add listener to upcoming events card + binding.upcomingEventsCard.setOnClickListener( + v -> + LaoDetailActivity.setCurrentFragment( + getParentFragmentManager(), + R.id.fragment_upcoming_events, + UpcomingEventsFragment::newInstance)); + return binding.getRoot(); } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index f2b1dbacca..5effe87162 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -60,6 +60,9 @@ private void subscribeToEventSet(Observable> observable) { public abstract void updateEventSet(List events); public List getEvents() { + if (events == null) { + return new ArrayList<>(); + } return new ArrayList<>(events); } diff --git a/fe2-android/app/src/main/res/layout/event_header_layout.xml b/fe2-android/app/src/main/res/layout/event_header_layout.xml index 380825012f..0ae203caa8 100644 --- a/fe2-android/app/src/main/res/layout/event_header_layout.xml +++ b/fe2-android/app/src/main/res/layout/event_header_layout.xml @@ -3,7 +3,7 @@ android:id="@+id/header_layout" android:layout_width="match_parent" android:layout_height="@dimen/header_card_view_height" - android:layout_marginTop="@dimen/event_list_sections_margin" + android:layout_marginTop="@dimen/events_header_top_margin" xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + diff --git a/fe2-android/app/src/main/res/values/dimens.xml b/fe2-android/app/src/main/res/values/dimens.xml index 579ee9d7c1..1738628d95 100644 --- a/fe2-android/app/src/main/res/values/dimens.xml +++ b/fe2-android/app/src/main/res/values/dimens.xml @@ -100,6 +100,7 @@ 8dp 16dp 2dp + 8dp 28dp From 4fdcfcc2c31ced0cb8e26c43e7887b93137e0519 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 02:16:46 +0100 Subject: [PATCH 039/121] Link upcoming event card to fragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListFragment.java | 48 ++++++++++++++----- .../main/res/layout/event_list_fragment.xml | 15 +++++- .../main/res/layout/token_list_fragment.xml | 2 +- .../app/src/main/res/values/dimens.xml | 2 + .../app/src/main/res/values/strings.xml | 5 ++ 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index 830f213133..e4943ea80f 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -14,8 +14,7 @@ import com.github.dedis.popstellar.R; import com.github.dedis.popstellar.databinding.EventListFragmentBinding; import com.github.dedis.popstellar.model.Role; -import com.github.dedis.popstellar.model.objects.event.EventState; -import com.github.dedis.popstellar.model.objects.event.EventType; +import com.github.dedis.popstellar.model.objects.event.*; import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; @@ -26,6 +25,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.gson.Gson; +import java.util.Set; + import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; @@ -71,21 +72,16 @@ public View onCreateView( binding.addRollCall.setOnClickListener(openCreateEvent(EventType.ROLL_CALL)); binding.addRollCallText.setOnClickListener(openCreateEvent(EventType.ROLL_CALL)); - // Observing events so that we know when to display the upcoming events card + // Observing events so that we know when to display the upcoming events card and displaying the + // Empty events text viewModel.addDisposable( viewModel .getEvents() .subscribe( - events -> - binding.upcomingEventsCard.setVisibility( - events.stream() - .anyMatch( // We are looking for any event that is in future section - event -> - // We want created events that are in more than 24 hours - event.getState().equals(EventState.CREATED) - && !event.isEventToday()) - ? View.VISIBLE - : View.GONE), + events -> { + setupUpcomingEventsCard(events); + setupEmptyEventsTextVisibility(events); + }, error -> ErrorUtils.logAndShow(requireContext(), TAG, R.string.error_event_observed))); @@ -97,6 +93,17 @@ public View onCreateView( R.id.fragment_upcoming_events, UpcomingEventsFragment::newInstance)); + // Observe role to match empty event text to it + viewModel + .getRole() + .observe( + getViewLifecycleOwner(), + role -> + binding.emptyEventsText.setText( + role.equals(Role.ORGANIZER) + ? R.string.empty_events_organizer_text + : R.string.empty_events_non_organizer_text)); + return binding.getRoot(); } @@ -164,4 +171,19 @@ private void setupEventListAdapter() { eventList.setAdapter(eventListAdapter); } + + private void setupUpcomingEventsCard(Set events) { + binding.upcomingEventsCard.setVisibility( + events.stream() + .anyMatch( // We are looking for any event that is in future section + event -> + // We want created events that are in more than 24 hours + event.getState().equals(EventState.CREATED) && !event.isEventToday()) + ? View.VISIBLE + : View.GONE); + } + + private void setupEmptyEventsTextVisibility(Set events) { + binding.emptyEventsLayout.setVisibility(events.isEmpty() ? View.VISIBLE : View.GONE); + } } diff --git a/fe2-android/app/src/main/res/layout/event_list_fragment.xml b/fe2-android/app/src/main/res/layout/event_list_fragment.xml index 6bffaf391b..458ede7dfd 100644 --- a/fe2-android/app/src/main/res/layout/event_list_fragment.xml +++ b/fe2-android/app/src/main/res/layout/event_list_fragment.xml @@ -145,5 +145,18 @@ app:layout_constraintBottom_toBottomOf="@id/add_roll_call" app:layout_constraintEnd_toStartOf="@id/add_roll_call" /> - + + + + + + diff --git a/fe2-android/app/src/main/res/layout/token_list_fragment.xml b/fe2-android/app/src/main/res/layout/token_list_fragment.xml index ac395fc375..e378faea8d 100644 --- a/fe2-android/app/src/main/res/layout/token_list_fragment.xml +++ b/fe2-android/app/src/main/res/layout/token_list_fragment.xml @@ -58,7 +58,7 @@ android:id="@+id/empty_token_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/margin_text_small" + android:layout_margin="@dimen/main_horizontal_margin" app:layout_constraintTop_toTopOf="parent"> 20dp 16dp + + 16dp diff --git a/fe2-android/app/src/main/res/values/strings.xml b/fe2-android/app/src/main/res/values/strings.xml index 28c8fad85d..ffcf8343cb 100644 --- a/fe2-android/app/src/main/res/values/strings.xml +++ b/fe2-android/app/src/main/res/values/strings.xml @@ -50,6 +50,11 @@ Connected to server Identifier Name + No event has been created in this LAO ! \n\nTo create one, tap on add button with a + on the bottom right + + No event has been created in this LAO ! \n\n + Since you are not an organizer, you must wait for them to create one. + Open From 72cdd64a625b682dc55ae142fbae70e985064b6f Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 02:17:09 +0100 Subject: [PATCH 040/121] Fix adapter bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListAdapter.java | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java index 8229554ca6..2ceb986600 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java @@ -1,8 +1,13 @@ package com.github.dedis.popstellar.ui.detail.event.eventlist; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PAST; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PRESENT; + import android.annotation.SuppressLint; import android.util.Log; -import android.view.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -18,12 +23,13 @@ import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; -import java.util.*; - import io.reactivex.Observable; -import static com.github.dedis.popstellar.model.objects.event.EventCategory.PAST; -import static com.github.dedis.popstellar.model.objects.event.EventCategory.PRESENT; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; public class EventListAdapter extends EventsAdapter { @@ -159,8 +165,11 @@ private Event getEvent(int position) { eventAccumulator += nbrOfPresentEvents; } if (expanded[PAST.ordinal()] && position <= nbrOfPastEvents + eventAccumulator + 1) { - return Objects.requireNonNull(eventsMap.get(PAST)).get(position - eventAccumulator - 2); + int secondSectionOffset = nbrOfPresentEvents > 0 ? 2 : 1; + return Objects.requireNonNull(eventsMap.get(PAST)) + .get(position - eventAccumulator - secondSectionOffset); } + Log.e(TAG, "position was " + position); throw new IllegalStateException("no event matches"); } @@ -172,7 +181,10 @@ private Event getEvent(int position) { private EventCategory getHeaderCategory(int position) { int nbrOfPresentEvents = Objects.requireNonNull(eventsMap.get(PRESENT)).size(); if (position == 0) { - return PRESENT; + // If this function is called, it means that getSize() > 0. Therefore there are some events + // in either past or present (or both). If there are none in the present the first item is the + // past header + return nbrOfPresentEvents > 0 ? PRESENT : PAST; } int eventAccumulator = 0; if (expanded[PRESENT.ordinal()]) { @@ -191,13 +203,19 @@ public int getItemCount() { int nbrOfPastEvents = eventsMap.get(PAST).size(); int eventAccumulator = 0; - if (expanded[PRESENT.ordinal()]) { - eventAccumulator = nbrOfPresentEvents; + if (nbrOfPresentEvents > 0) { + if (expanded[PRESENT.ordinal()]) { + eventAccumulator = nbrOfPresentEvents; + } + eventAccumulator++; // If there are present events, we want to display the header as well } - if (expanded[PAST.ordinal()]) { - eventAccumulator += nbrOfPastEvents; + if (nbrOfPastEvents > 0) { + if (expanded[PAST.ordinal()]) { + eventAccumulator += nbrOfPastEvents; + } + eventAccumulator++; // If there are past events, we want to display the header as well } - return eventAccumulator + 2; // The number expanded of events + the 2 sub-headers + return eventAccumulator; } @Override @@ -211,7 +229,8 @@ public int getItemViewType(int position) { if (position == 0) { return TYPE_HEADER; } - if (position == eventAccumulator + 1) { + + if (position == eventAccumulator + 1 && nbrOfPresentEvents > 0) { return TYPE_HEADER; } return TYPE_EVENT; From a3b363c72134af48eb381015e92cde7b5f0c2a2c Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 17:09:51 +0100 Subject: [PATCH 041/121] Extend Event interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../com/github/dedis/popstellar/model/objects/Election.java | 1 + .../com/github/dedis/popstellar/model/objects/RollCall.java | 1 + .../github/dedis/popstellar/model/objects/event/Event.java | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java index bacdcac191..df94b97209 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java @@ -101,6 +101,7 @@ public String getElectionKey() { return electionKey; } + @Override public String getName() { return name; } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/RollCall.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/RollCall.java index 62bda5c913..955bd7ea5b 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/RollCall.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/RollCall.java @@ -56,6 +56,7 @@ public String getPersistentId() { return persistentId; } + @Override public String getName() { return name; } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java index 068474e134..f3de883ae9 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java @@ -17,6 +17,8 @@ public long getStartTimestampInMillis() { public abstract EventState getState(); + public abstract String getName(); + public long getEndTimestampInMillis() { return getEndTimestamp() * 1000; } @@ -38,4 +40,8 @@ public boolean isEventToday() { long currentTime = System.currentTimeMillis(); return getEndTimestampInMillis() - currentTime < Constants.MS_IN_A_DAY; } + + public boolean isStartPassed() { + return System.currentTimeMillis() >= getStartTimestampInMillis(); + } } From e664cb785361e85a2178bee5c7bcbdf700102d8a Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 17:10:16 +0100 Subject: [PATCH 042/121] Update gradle to add pretty time lib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- fe2-android/app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fe2-android/app/build.gradle b/fe2-android/app/build.gradle index fb2748c569..2432a76b2e 100644 --- a/fe2-android/app/build.gradle +++ b/fe2-android/app/build.gradle @@ -265,6 +265,9 @@ dependencies { testImplementation "org.slf4j:slf4j-api:$slf4j_version" testImplementation "org.slf4j:slf4j-simple:$slf4j_version" + // ================= Pretty Time ================= + implementation 'org.ocpsoft.prettytime:prettytime:5.0.4.Final' + // ============= Test Framework ============ debugImplementation 'junit:junit:4.13.2' // Android framework From d76b508c2e9054a66e602dbb473bd9fb4cb880a4 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 17:16:25 +0100 Subject: [PATCH 043/121] Add user friendly time on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../detail/event/eventlist/EventsAdapter.java | 37 +++++++++++++++++-- .../app/src/main/res/layout/event_layout.xml | 36 ++++++++++++++---- .../app/src/main/res/values/strings.xml | 6 +-- .../app/src/main/res/values/styles.xml | 2 +- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index 5effe87162..ad220870c4 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -25,6 +25,8 @@ import com.github.dedis.popstellar.utility.error.UnknownLaoException; import com.github.dedis.popstellar.utility.error.keys.KeyException; +import org.ocpsoft.prettytime.PrettyTime; + import java.util.*; import java.util.stream.Collectors; @@ -86,7 +88,6 @@ protected void handleEventContent(EventViewHolder eventViewHolder, Event event) if (event.getType().equals(EventType.ELECTION)) { eventViewHolder.eventIcon.setImageResource(R.drawable.ic_vote); Election election = (Election) event; - eventViewHolder.eventTitle.setText(election.getName()); View.OnClickListener listener = view -> LaoDetailActivity.setCurrentFragment( @@ -98,7 +99,6 @@ protected void handleEventContent(EventViewHolder eventViewHolder, Event event) } else if (event.getType().equals(EventType.ROLL_CALL)) { eventViewHolder.eventIcon.setImageResource(R.drawable.ic_roll_call); RollCall rollCall = (RollCall) event; - eventViewHolder.eventTitle.setText(rollCall.getName()); eventViewHolder.eventCard.setOnClickListener( view -> { if (viewModel.isWalletSetup()) { @@ -119,8 +119,37 @@ protected void handleEventContent(EventViewHolder eventViewHolder, Event event) showWalletNotSetupWarning(); } }); - eventViewHolder.eventTitle.setText(rollCall.getName()); } + eventViewHolder.eventTitle.setText(event.getName()); + handleTimeAndLocation(eventViewHolder, event); + } + + private void handleTimeAndLocation(EventViewHolder viewHolder, Event event) { + String location = ""; + if (event instanceof RollCall) { + location = ", at " + ((RollCall) event).getLocation(); + } + String timeText = ""; + switch (event.getState()) { + case CREATED: + if (event.isStartPassed()) { + timeText = getActivity().getString(R.string.start_anytime); + } else { + long eventTime = event.getStartTimestampInMillis(); + timeText = "Starting " + new PrettyTime().format(new Date(eventTime)); + } + break; + case OPENED: + timeText = getActivity().getString(R.string.ongoing); + break; + + case CLOSED: + case RESULTS_READY: + long eventTime = event.getEndTimestampInMillis(); + timeText = "Closed " + new PrettyTime().format(new Date(eventTime)); + } + String textToDisplay = timeText + location; + viewHolder.eventTimeAndLoc.setText(textToDisplay); } public void showWalletNotSetupWarning() { @@ -134,12 +163,14 @@ public static class EventViewHolder extends RecyclerView.ViewHolder { private final TextView eventTitle; private final ImageView eventIcon; private final CardView eventCard; + private final TextView eventTimeAndLoc; public EventViewHolder(@NonNull View itemView) { super(itemView); eventTitle = itemView.findViewById(R.id.event_card_text_view); eventIcon = itemView.findViewById(R.id.event_type_image); eventCard = itemView.findViewById(R.id.event_card_view); + eventTimeAndLoc = itemView.findViewById(R.id.event_card_time_and_loc); } } } diff --git a/fe2-android/app/src/main/res/layout/event_layout.xml b/fe2-android/app/src/main/res/layout/event_layout.xml index cbcc441074..731f68b1fa 100644 --- a/fe2-android/app/src/main/res/layout/event_layout.xml +++ b/fe2-android/app/src/main/res/layout/event_layout.xml @@ -9,7 +9,7 @@ + android:layout_height="match_parent" + android:paddingVertical="@dimen/padding_standard"> - + app:layout_constraintEnd_toStartOf="@id/event_card_right_arrow"> + + + + + + Name No event has been created in this LAO ! \n\nTo create one, tap on add button with a + on the bottom right - No event has been created in this LAO ! \n\n - Since you are not an organizer, you must wait for them to create one. + No event has been created in this LAO ! \n\nSince you are not an organizer, you must wait for them to create one. @@ -71,7 +70,8 @@ Start time must be before end time End time must be after start time End Time - + Ongoing + Starting anytime now Cancel diff --git a/fe2-android/app/src/main/res/values/styles.xml b/fe2-android/app/src/main/res/values/styles.xml index 89e5f33c2f..310d1873fd 100644 --- a/fe2-android/app/src/main/res/values/styles.xml +++ b/fe2-android/app/src/main/res/values/styles.xml @@ -51,7 +51,7 @@ @dimen/properties_section_margin 22dp bold - @color/black + 0.87 center From 5962bc2a4c004c29fdd904c7799b9f1fc9804f9c Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 20:48:01 +0100 Subject: [PATCH 044/121] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../popstellar/ui/detail/EventListAdapterTest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java index aef00bc387..b8e89b6629 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java @@ -114,7 +114,7 @@ public void emptyAdapterTest() { EventListAdapter adapter = getEventListAdapter(); events.onNext(Collections.emptySet()); - assertEquals(3, adapter.getItemCount()); + assertEquals(0, adapter.getItemCount()); } @Test @@ -123,21 +123,18 @@ public void updateAreDisplayed() { // Default values events.onNext(Sets.newSet(ROLL_CALL, ROLL_CALL2)); - + System.out.println(adapter.getItemCount()); assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(0)); - assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(1)); + assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(1)); assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(2)); - assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(3)); - assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(4)); // Open rollcall 1 should move it from - events.onNext(Sets.newSet(RollCall.openRollCall(ROLL_CALL), ROLL_CALL2)); + events.onNext(Sets.newSet(RollCall.closeRollCall(ROLL_CALL), ROLL_CALL2)); assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(0)); assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(1)); assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(2)); assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(3)); - assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(4)); } private EventListAdapter getEventListAdapter() { From 012b28a48285d1f86ebea300c21ba42def678f07 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 22:25:37 +0100 Subject: [PATCH 045/121] Fix code smells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../event/eventlist/EventListAdapter.java | 36 ++++---------- .../detail/event/eventlist/EventsAdapter.java | 48 +++++++------------ .../eventlist/UpcomingEventsAdapter.java | 4 +- .../app/src/main/res/values/dimens.xml | 3 -- 4 files changed, 30 insertions(+), 61 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java index 2ceb986600..d3d5796515 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java @@ -1,18 +1,12 @@ package com.github.dedis.popstellar.ui.detail.event.eventlist; -import static com.github.dedis.popstellar.model.objects.event.EventCategory.PAST; -import static com.github.dedis.popstellar.model.objects.event.EventCategory.PRESENT; - import android.annotation.SuppressLint; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; +import android.view.*; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; @@ -23,13 +17,12 @@ import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import com.github.dedis.popstellar.ui.detail.event.LaoDetailAnimation; +import java.util.*; + import io.reactivex.Observable; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PAST; +import static com.github.dedis.popstellar.model.objects.event.EventCategory.PRESENT; public class EventListAdapter extends EventsAdapter { @@ -112,14 +105,12 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi ImageView expandIcon = headerViewHolder.expandIcon; ConstraintLayout headerLayout = headerViewHolder.headerLayout; - switch (eventCategory) { - case PRESENT: - headerTitle.setText(R.string.present_header_title); - break; - case PAST: - headerTitle.setText(R.string.past_header_title); - break; + if (eventCategory == PRESENT) { + headerTitle.setText(R.string.present_header_title); + } else if (eventCategory == PAST) { + headerTitle.setText(R.string.past_header_title); } + expandIcon.setRotation(expanded[eventCategory.ordinal()] ? 180f : 0f); String numberOfEventsInCategory = @@ -236,13 +227,6 @@ public int getItemViewType(int position) { return TYPE_EVENT; } - public void showWalletNotSetupWarning() { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle("You have to setup up your wallet before connecting."); - builder.setPositiveButton("Ok", (dialog, which) -> dialog.dismiss()); - builder.show(); - } - public static class HeaderViewHolder extends RecyclerView.ViewHolder { private final TextView headerTitle; private final TextView headerNumber; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index ad220870c4..8348c85197 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -6,7 +6,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.cardview.widget.CardView; import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; @@ -38,25 +37,25 @@ public abstract class EventsAdapter extends RecyclerView.Adapter events; private final LaoDetailViewModel viewModel; private final FragmentActivity activity; - private final String TAG; + private final String tag; - public EventsAdapter( + protected EventsAdapter( Observable> observable, LaoDetailViewModel viewModel, FragmentActivity activity, - String TAG) { + String tag) { this.viewModel = viewModel; this.activity = activity; - this.TAG = TAG; + this.tag = tag; subscribeToEventSet(observable); } private void subscribeToEventSet(Observable> observable) { this.viewModel.addDisposable( observable - .map(events -> events.stream().sorted().collect(Collectors.toList())) + .map(eventList -> eventList.stream().sorted().collect(Collectors.toList())) // No need to check for error as the events errors already handles them - .subscribe(this::updateEventSet, err -> Log.e(TAG, "ERROR", err))); + .subscribe(this::updateEventSet, err -> Log.e(tag, "ERROR", err))); } public abstract void updateEventSet(List events); @@ -101,22 +100,18 @@ protected void handleEventContent(EventViewHolder eventViewHolder, Event event) RollCall rollCall = (RollCall) event; eventViewHolder.eventCard.setOnClickListener( view -> { - if (viewModel.isWalletSetup()) { - try { - PoPToken token = viewModel.getCurrentPopToken(rollCall); - setCurrentFragment( - activity.getSupportFragmentManager(), - R.id.fragment_roll_call, - () -> - RollCallFragment.newInstance( - token.getPublicKey(), rollCall.getPersistentId())); - } catch (KeyException e) { - ErrorUtils.logAndShow(activity, TAG, e, R.string.key_generation_exception); - } catch (UnknownLaoException e) { - ErrorUtils.logAndShow(activity, TAG, e, R.string.error_no_lao); - } - } else { - showWalletNotSetupWarning(); + try { + PoPToken token = viewModel.getCurrentPopToken(rollCall); + setCurrentFragment( + activity.getSupportFragmentManager(), + R.id.fragment_roll_call, + () -> + RollCallFragment.newInstance( + token.getPublicKey(), rollCall.getPersistentId())); + } catch (KeyException e) { + ErrorUtils.logAndShow(activity, tag, e, R.string.key_generation_exception); + } catch (UnknownLaoException e) { + ErrorUtils.logAndShow(activity, tag, e, R.string.error_no_lao); } }); } @@ -152,13 +147,6 @@ private void handleTimeAndLocation(EventViewHolder viewHolder, Event event) { viewHolder.eventTimeAndLoc.setText(textToDisplay); } - public void showWalletNotSetupWarning() { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("You have to setup up your wallet before connecting."); - builder.setPositiveButton("Ok", (dialog, which) -> dialog.dismiss()); - builder.show(); - } - public static class EventViewHolder extends RecyclerView.ViewHolder { private final TextView eventTitle; private final ImageView eventIcon; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java index 7280f55404..87837c2ce9 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java @@ -25,8 +25,8 @@ public UpcomingEventsAdapter( Observable> observable, LaoDetailViewModel viewModel, FragmentActivity activity, - String TAG) { - super(observable, viewModel, activity, TAG); + String tag) { + super(observable, viewModel, activity, tag); } @SuppressLint("NotifyDataSetChanged") diff --git a/fe2-android/app/src/main/res/values/dimens.xml b/fe2-android/app/src/main/res/values/dimens.xml index cdf618fb5f..b5cb3228d2 100644 --- a/fe2-android/app/src/main/res/values/dimens.xml +++ b/fe2-android/app/src/main/res/values/dimens.xml @@ -64,11 +64,9 @@ 20dp 30dp 8dp - 55dp 16dp - 8dp 16sp 45dp 18sp @@ -109,7 +107,6 @@ 8dp - 40dp 30dp From e01f75f19f37878dbd5fef33684ed085e368040c Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Wed, 25 Jan 2023 22:32:15 +0100 Subject: [PATCH 046/121] Move tests in new package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../detail/{ => event/eventlist}/EventListAdapterTest.java | 5 +++-- .../detail/{ => event/eventlist}/EventListFragmentTest.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) rename fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/{ => event/eventlist}/EventListAdapterTest.java (96%) rename fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/{ => event/eventlist}/EventListFragmentTest.java (98%) diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapterTest.java similarity index 96% rename from fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java rename to fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapterTest.java index b8e89b6629..b28f189ba6 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapterTest.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -7,7 +7,8 @@ import com.github.dedis.popstellar.model.objects.event.Event; import com.github.dedis.popstellar.model.objects.event.EventState; import com.github.dedis.popstellar.testutils.*; -import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListAdapter; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; +import com.github.dedis.popstellar.ui.detail.LaoDetailViewModel; import org.junit.Rule; import org.junit.Test; diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragmentTest.java similarity index 98% rename from fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java rename to fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragmentTest.java index 81bb8c2f5f..1f40d29d89 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListFragmentTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragmentTest.java @@ -1,4 +1,4 @@ -package com.github.dedis.popstellar.ui.detail; +package com.github.dedis.popstellar.ui.detail.event.eventlist; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -22,6 +22,7 @@ import com.github.dedis.popstellar.repository.remote.GlobalNetworkManager; import com.github.dedis.popstellar.repository.remote.MessageSender; import com.github.dedis.popstellar.testutils.*; +import com.github.dedis.popstellar.ui.detail.LaoDetailActivity; import com.github.dedis.popstellar.utility.error.keys.KeyException; import com.github.dedis.popstellar.utility.handler.MessageHandler; import com.github.dedis.popstellar.utility.security.KeyManager; From 4a88bf3d64af43ee7aba40fc99d3ca7b5a94d90b Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Fri, 27 Jan 2023 12:26:18 +0100 Subject: [PATCH 047/121] Modularity improvement --- .../detail/event/eventlist/EventsAdapter.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index 8348c85197..2dd20f507e 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -85,40 +85,43 @@ public FragmentActivity getActivity() { */ protected void handleEventContent(EventViewHolder eventViewHolder, Event event) { if (event.getType().equals(EventType.ELECTION)) { - eventViewHolder.eventIcon.setImageResource(R.drawable.ic_vote); - Election election = (Election) event; - View.OnClickListener listener = - view -> - LaoDetailActivity.setCurrentFragment( - activity.getSupportFragmentManager(), - R.id.fragment_election, - () -> ElectionFragment.newInstance(election.getId())); - eventViewHolder.eventCard.setOnClickListener(listener); - + handleElectionContent(eventViewHolder, (Election) event); } else if (event.getType().equals(EventType.ROLL_CALL)) { - eventViewHolder.eventIcon.setImageResource(R.drawable.ic_roll_call); - RollCall rollCall = (RollCall) event; - eventViewHolder.eventCard.setOnClickListener( - view -> { - try { - PoPToken token = viewModel.getCurrentPopToken(rollCall); - setCurrentFragment( - activity.getSupportFragmentManager(), - R.id.fragment_roll_call, - () -> - RollCallFragment.newInstance( - token.getPublicKey(), rollCall.getPersistentId())); - } catch (KeyException e) { - ErrorUtils.logAndShow(activity, tag, e, R.string.key_generation_exception); - } catch (UnknownLaoException e) { - ErrorUtils.logAndShow(activity, tag, e, R.string.error_no_lao); - } - }); + handleRollCallContent(eventViewHolder, (RollCall) event); } eventViewHolder.eventTitle.setText(event.getName()); handleTimeAndLocation(eventViewHolder, event); } + private void handleElectionContent(EventViewHolder eventViewHolder, Election election) { + eventViewHolder.eventIcon.setImageResource(R.drawable.ic_vote); + eventViewHolder.eventCard.setOnClickListener( + view -> + LaoDetailActivity.setCurrentFragment( + activity.getSupportFragmentManager(), + R.id.fragment_election, + () -> ElectionFragment.newInstance(election.getId()))); + } + + private void handleRollCallContent(EventViewHolder eventViewHolder, RollCall rollCall) { + eventViewHolder.eventIcon.setImageResource(R.drawable.ic_roll_call); + eventViewHolder.eventCard.setOnClickListener( + view -> { + try { + PoPToken token = viewModel.getCurrentPopToken(rollCall); + setCurrentFragment( + activity.getSupportFragmentManager(), + R.id.fragment_roll_call, + () -> + RollCallFragment.newInstance(token.getPublicKey(), rollCall.getPersistentId())); + } catch (KeyException e) { + ErrorUtils.logAndShow(activity, tag, e, R.string.key_generation_exception); + } catch (UnknownLaoException e) { + ErrorUtils.logAndShow(activity, tag, e, R.string.error_no_lao); + } + }); + } + private void handleTimeAndLocation(EventViewHolder viewHolder, Event event) { String location = ""; if (event instanceof RollCall) { From 9bcc3870e3417c18891266439520a087cf442c51 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Fri, 27 Jan 2023 13:07:02 +0100 Subject: [PATCH 048/121] Fix error message syntax --- fe2-android/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe2-android/app/src/main/res/values/strings.xml b/fe2-android/app/src/main/res/values/strings.xml index 4bd1813d38..ddfea020c5 100644 --- a/fe2-android/app/src/main/res/values/strings.xml +++ b/fe2-android/app/src/main/res/values/strings.xml @@ -268,7 +268,7 @@ An error occurred during the storage of the wallet seed Error, this QR code is not a pop token or the key format is invalid Error, this QR code is not a main public key or the key format is invalid - Error, updating events unsuccessful + Error, could not update events An error response was replied by the server\nError %1$d - %2$s From c3ca8af3556620d46d1dd18bee205aef549945bc Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Fri, 27 Jan 2023 13:07:16 +0100 Subject: [PATCH 049/121] Fix stale comment --- .../ui/detail/EventListAdapterTest.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java index b8e89b6629..4e76a4e41c 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/detail/EventListAdapterTest.java @@ -1,12 +1,22 @@ package com.github.dedis.popstellar.ui.detail; +import static com.github.dedis.popstellar.testutils.pages.detail.LaoDetailActivityPageObject.fragmentToOpenExtra; +import static com.github.dedis.popstellar.testutils.pages.detail.LaoDetailActivityPageObject.laoDetailValue; +import static com.github.dedis.popstellar.testutils.pages.detail.LaoDetailActivityPageObject.laoIdExtra; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.github.dedis.popstellar.model.objects.*; +import com.github.dedis.popstellar.model.objects.Lao; +import com.github.dedis.popstellar.model.objects.RollCall; +import com.github.dedis.popstellar.model.objects.Wallet; import com.github.dedis.popstellar.model.objects.event.Event; import com.github.dedis.popstellar.model.objects.event.EventState; -import com.github.dedis.popstellar.testutils.*; +import com.github.dedis.popstellar.testutils.Base64DataUtils; +import com.github.dedis.popstellar.testutils.BundleBuilder; +import com.github.dedis.popstellar.testutils.IntentUtils; import com.github.dedis.popstellar.ui.detail.event.eventlist.EventListAdapter; import org.junit.Rule; @@ -19,17 +29,17 @@ import org.mockito.junit.MockitoTestRule; import java.security.GeneralSecurityException; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import dagger.hilt.android.testing.*; +import dagger.hilt.android.testing.BindValue; +import dagger.hilt.android.testing.HiltAndroidRule; +import dagger.hilt.android.testing.HiltAndroidTest; import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.Subject; -import static com.github.dedis.popstellar.testutils.pages.detail.LaoDetailActivityPageObject.*; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.when; - @HiltAndroidTest @RunWith(AndroidJUnit4.class) public class EventListAdapterTest { @@ -123,12 +133,11 @@ public void updateAreDisplayed() { // Default values events.onNext(Sets.newSet(ROLL_CALL, ROLL_CALL2)); - System.out.println(adapter.getItemCount()); assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(0)); assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(1)); assertEquals(EventListAdapter.TYPE_EVENT, adapter.getItemViewType(2)); - // Open rollcall 1 should move it from + // Close rollcall 1 should move it from current to past events.onNext(Sets.newSet(RollCall.closeRollCall(ROLL_CALL), ROLL_CALL2)); assertEquals(EventListAdapter.TYPE_HEADER, adapter.getItemViewType(0)); From 94507b991ed99a99845d2e2c076ee33c85ca6490 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Fri, 27 Jan 2023 13:07:42 +0100 Subject: [PATCH 050/121] Improve naming --- .../com/github/dedis/popstellar/model/objects/event/Event.java | 2 +- .../popstellar/ui/detail/event/eventlist/EventListAdapter.java | 2 +- .../ui/detail/event/eventlist/EventListFragment.java | 2 +- .../ui/detail/event/eventlist/UpcomingEventsAdapter.java | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java index f3de883ae9..096c3f9258 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java @@ -36,7 +36,7 @@ public int compareTo(Event o) { /** * @return true if event the event takes place today */ - public boolean isEventToday() { + public boolean isEventEndingToday() { long currentTime = System.currentTimeMillis(); return getEndTimestampInMillis() - currentTime < Constants.MS_IN_A_DAY; } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java index d3d5796515..53da29b357 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListAdapter.java @@ -60,7 +60,7 @@ public void updateEventSet(List events) { for (Event event : events) { switch (event.getState()) { case CREATED: - if (event.isEventToday()) { + if (event.isEventEndingToday()) { eventsMap.get(PRESENT).add(event); } break; diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java index e4943ea80f..225603a8d9 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventListFragment.java @@ -178,7 +178,7 @@ private void setupUpcomingEventsCard(Set events) { .anyMatch( // We are looking for any event that is in future section event -> // We want created events that are in more than 24 hours - event.getState().equals(EventState.CREATED) && !event.isEventToday()) + event.getState().equals(EventState.CREATED) && !event.isEventEndingToday()) ? View.VISIBLE : View.GONE); } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java index 87837c2ce9..7366cd6085 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/UpcomingEventsAdapter.java @@ -34,7 +34,8 @@ public UpcomingEventsAdapter( public void updateEventSet(List events) { this.setEvents( events.stream() - .filter(event -> event.getState().equals(EventState.CREATED) && !event.isEventToday()) + .filter( + event -> event.getState().equals(EventState.CREATED) && !event.isEventEndingToday()) .collect(Collectors.toList())); notifyDataSetChanged(); } From be780ba8bfee2cfb12a26ee834a7b8b006f2b05d Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Fri, 27 Jan 2023 16:22:13 +0100 Subject: [PATCH 051/121] Update comment --- .../com/github/dedis/popstellar/model/objects/event/Event.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java index 096c3f9258..3c4c10a79b 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/event/Event.java @@ -34,7 +34,7 @@ public int compareTo(Event o) { } /** - * @return true if event the event takes place today + * @return true if event the event takes place within 24 hours */ public boolean isEventEndingToday() { long currentTime = System.currentTimeMillis(); From 2c8c4ee3d08f11e2ca5d46893c2364b0db6c468a Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 13:15:53 +0100 Subject: [PATCH 052/121] fix tentative when catching up messages with same timestamp --- be1-go/inbox/mod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be1-go/inbox/mod.go b/be1-go/inbox/mod.go index 3f7e41068a..810555e9e3 100644 --- a/be1-go/inbox/mod.go +++ b/be1-go/inbox/mod.go @@ -81,7 +81,7 @@ func (i *Inbox) GetSortedMessages() []message.Message { } // sort.Slice on messages based on the timestamp - sort.Slice(messages, func(i, j int) bool { + sort.SliceStable(messages, func(i, j int) bool { return messages[i].storedTime < messages[j].storedTime }) From 0df3e42720cd35b590ae5010e6386337cbe1d4cf Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 13:16:37 +0100 Subject: [PATCH 053/121] remove added checks --- be1-go/channel/lao/mod_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/be1-go/channel/lao/mod_test.go b/be1-go/channel/lao/mod_test.go index dfcbd98154..62a3bfeb34 100644 --- a/be1-go/channel/lao/mod_test.go +++ b/be1-go/channel/lao/mod_test.go @@ -211,9 +211,6 @@ func TestLAOChannel_Catchup(t *testing.T) { laoChannel, ok := channel.(*Channel) require.True(t, ok) - _, ok = laoChannel.inbox.GetMessage(messages[0].MessageID) - require.True(t, ok) - time.Sleep(time.Millisecond) for i := 2; i < numMessages+1; i++ { From 5b7126346859196b10fa3ed902ba9b2ead74e322 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 13:17:31 +0100 Subject: [PATCH 054/121] modified mock clock usage to try and fix test --- be1-go/channel/consensus/mod_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/be1-go/channel/consensus/mod_test.go b/be1-go/channel/consensus/mod_test.go index ba2df9120e..96351db0ff 100644 --- a/be1-go/channel/consensus/mod_test.go +++ b/be1-go/channel/consensus/mod_test.go @@ -1458,14 +1458,13 @@ func Test_Timeout_Prepare(t *testing.T) { require.NoError(t, err) // Verify that a prepare message was sent and empty the socket - // Unmarshal the failure message sent to other servers to verify its values var sentPrepare method.Publish err = json.Unmarshal(fakeHub.fakeSock.msg, &sentPrepare) require.NoError(t, err) sentMsg := sentPrepare.Params.Message - // Unmarshal the failure message data to check its values + // Unmarshal the prepare message data to check its values jsonData, err := base64.URLEncoding.DecodeString(sentMsg.Data) require.NoError(t, err) var prepare messagedata.ConsensusPrepare @@ -1477,7 +1476,9 @@ func Test_Timeout_Prepare(t *testing.T) { fakeHub.fakeSock.msg = nil - clock.Add(12 * time.Second) + clock.Add(4 * time.Second) + time.Sleep(500 * time.Millisecond) + clock.Add(4 * time.Second) time.Sleep(500 * time.Millisecond) // A failure message should be sent to the socket From 4bdcf290e24a13e350f1b0363648ea679ce03b0d Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 13:18:00 +0100 Subject: [PATCH 055/121] get more infos for election failure --- be1-go/channel/election/mod_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be1-go/channel/election/mod_test.go b/be1-go/channel/election/mod_test.go index 2ddcb3ef47..b061890a37 100644 --- a/be1-go/channel/election/mod_test.go +++ b/be1-go/channel/election/mod_test.go @@ -479,7 +479,7 @@ func Test_Sending_Election_Key(t *testing.T) { data := messagedata.ElectionKey{}.NewEmpty() err := electionKeyMsg.UnmarshalData(data) - require.NoError(t, err) + require.NoError(t, err, electionKeyMsg) dataKey, ok := data.(*messagedata.ElectionKey) require.True(t, ok) From a665cbe11205a92fabb972985361f002b49d00b6 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 14:08:23 +0100 Subject: [PATCH 056/121] second try to fix inbox --- be1-go/channel/lao/mod.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/be1-go/channel/lao/mod.go b/be1-go/channel/lao/mod.go index 71478108da..2db986202c 100644 --- a/be1-go/channel/lao/mod.go +++ b/be1-go/channel/lao/mod.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "sync" + "time" "go.dedis.ch/kyber/v3" @@ -128,6 +129,9 @@ func NewChannel(channelID string, hub channel.HubFunctionalities, msg message.Me newChannel.registry = newChannel.NewLAORegistry() + // sleep to avoid misordering messages + time.Sleep(time.Nanosecond) + err := newChannel.createAndSendLAOGreet() if err != nil { return nil, xerrors.Errorf("failed to send the greeting message: %v", err) From 56708b84eb964d7e95e520fa8709d10b79c1eebe Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Tue, 31 Jan 2023 16:50:28 +0100 Subject: [PATCH 057/121] cleanup msg order fix --- be1-go/channel/lao/mod.go | 4 ---- be1-go/inbox/mod.go | 25 +++++++++++-------------- be1-go/inbox/mod_test.go | 6 +++--- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/be1-go/channel/lao/mod.go b/be1-go/channel/lao/mod.go index 2db986202c..71478108da 100644 --- a/be1-go/channel/lao/mod.go +++ b/be1-go/channel/lao/mod.go @@ -26,7 +26,6 @@ import ( "strconv" "strings" "sync" - "time" "go.dedis.ch/kyber/v3" @@ -129,9 +128,6 @@ func NewChannel(channelID string, hub channel.HubFunctionalities, msg message.Me newChannel.registry = newChannel.NewLAORegistry() - // sleep to avoid misordering messages - time.Sleep(time.Nanosecond) - err := newChannel.createAndSendLAOGreet() if err != nil { return nil, xerrors.Errorf("failed to send the greeting message: %v", err) diff --git a/be1-go/inbox/mod.go b/be1-go/inbox/mod.go index 810555e9e3..ec7096c034 100644 --- a/be1-go/inbox/mod.go +++ b/be1-go/inbox/mod.go @@ -18,7 +18,8 @@ type messageInfo struct { // Inbox represents an in-memory data store to record incoming messages. type Inbox struct { mutex sync.RWMutex - msgs map[string]*messageInfo + msgsMap map[string]*messageInfo + msgsArray []*messageInfo channelID string } @@ -26,7 +27,8 @@ type Inbox struct { func NewInbox(channelID string) *Inbox { return &Inbox{ mutex: sync.RWMutex{}, - msgs: make(map[string]*messageInfo), + msgsMap: make(map[string]*messageInfo), + msgsArray: make([]*messageInfo, 0), channelID: channelID, } } @@ -66,7 +68,8 @@ func (i *Inbox) StoreMessage(msg message.Message) { storedTime: storedTime, } - i.msgs[msg.MessageID] = messageInfo + i.msgsMap[msg.MessageID] = messageInfo + i.msgsArray = append(i.msgsArray, messageInfo) } // GetSortedMessages returns all messages stored sorted by stored time. @@ -74,22 +77,16 @@ func (i *Inbox) GetSortedMessages() []message.Message { i.mutex.RLock() defer i.mutex.RUnlock() - messages := make([]messageInfo, 0, len(i.msgs)) - // iterate over map and collect all the values (messageInfo instances) - for _, msgInfo := range i.msgs { - messages = append(messages, *msgInfo) - } - // sort.Slice on messages based on the timestamp - sort.SliceStable(messages, func(i, j int) bool { - return messages[i].storedTime < messages[j].storedTime + sort.SliceStable(i.msgsArray, func(k, l int) bool { + return i.msgsArray[k].storedTime < i.msgsArray[l].storedTime }) - result := make([]message.Message, len(messages)) + result := make([]message.Message, len(i.msgsArray)) // iterate and extract the messages[i].message field and // append it to the result slice - for i, msgInfo := range messages { + for i, msgInfo := range i.msgsArray { result[i] = msgInfo.message } @@ -102,7 +99,7 @@ func (i *Inbox) GetMessage(messageID string) (*message.Message, bool) { i.mutex.Lock() defer i.mutex.Unlock() - msgInfo, ok := i.msgs[messageID] + msgInfo, ok := i.msgsMap[messageID] if !ok { return nil, false } diff --git a/be1-go/inbox/mod_test.go b/be1-go/inbox/mod_test.go index f512b4a9df..3f7f9c84af 100644 --- a/be1-go/inbox/mod_test.go +++ b/be1-go/inbox/mod_test.go @@ -20,7 +20,7 @@ func TestInbox_AddWitnessSignature(t *testing.T) { // Add a message to the inbox inbox.StoreMessage(msg) - require.Equal(t, 1, len(inbox.msgs)) + require.Equal(t, 1, len(inbox.msgsMap)) // Add the witness signature to the message in the inbox err := inbox.AddWitnessSignature(msg.MessageID, "456", "789") @@ -47,7 +47,7 @@ func TestInbox_AddSigWrongMessages(t *testing.T) { _, ok := inbox.GetMessage(string(buf)) require.False(t, ok) - require.Equal(t, 0, len(inbox.msgs)) + require.Equal(t, 0, len(inbox.msgsMap)) } func TestInbox_AddWitnessSignatures(t *testing.T) { @@ -58,7 +58,7 @@ func TestInbox_AddWitnessSignatures(t *testing.T) { // Add a message to the inbox inbox.StoreMessage(msg) - require.Equal(t, 1, len(inbox.msgs)) + require.Equal(t, 1, len(inbox.msgsMap)) signaturesNumber := 100 for i := 0; i < signaturesNumber; i++ { From 6629652f7810056c8a94397588f640b9934d822d Mon Sep 17 00:00:00 2001 From: GabrielFleischer Date: Tue, 31 Jan 2023 22:33:15 +0100 Subject: [PATCH 058/121] Fix vote encryption The order of ids was wrongly computed Signed-off-by: GabrielFleischer --- .../popstellar/model/objects/Election.java | 16 ++--- .../utility/handler/data/ElectionHandler.java | 65 +++++++++++-------- .../utility/handler/ElectionHandlerTest.java | 28 ++++---- 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java index bacdcac191..46ab2ac96e 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/Election.java @@ -33,7 +33,7 @@ public class Election extends Event { private final Map> votesBySender; // Map that associates each messageId to its sender - private final Map messageMap; + private final Map messageMap; private final EventState state; @@ -51,7 +51,7 @@ public Election( String electionKey, ElectionVersion electionVersion, Map> votesBySender, - Map messageMap, + Map messageMap, EventState state, Map> results) { @@ -72,8 +72,7 @@ public Election( this.votesBySender = Copyable.copyMapOfList(votesBySender); this.results = Copyable.copyMapOfSet(results); // Create message map as a tree map to sort messages correctly - this.messageMap = new TreeMap<>(Comparator.comparing(MessageID::getEncoded)); - this.messageMap.putAll(messageMap); + this.messageMap = new HashMap<>(messageMap); } private static void validateVotesTypes( @@ -129,7 +128,7 @@ public EventState getState() { return state; } - public Map getMessageMap() { + public Map getMessageMap() { return messageMap; } @@ -239,12 +238,13 @@ public static String generateEncryptedElectionVoteId( */ public String computeRegisteredVotesHash() { String[] ids = - messageMap.values().stream() + messageMap.keySet().stream() .map(votesBySender::get) // Merge lists and drop nulls .flatMap( electionVotes -> electionVotes != null ? electionVotes.stream() : Stream.empty()) .map(Vote::getId) + .sorted() .toArray(String[]::new); if (ids.length == 0) { @@ -330,7 +330,7 @@ public static class ElectionBuilder { private String electionKey; private ElectionVersion electionVersion; private final Map> votesBySender; - private final Map messageMap; + private final Map messageMap; private EventState state; private Map> results; @@ -408,7 +408,7 @@ public ElectionBuilder updateVotes(@NonNull PublicKey senderPk, @NonNull List= castVote.getCreation() || election.getState() != CLOSED) { // Retrieve previous cast vote message stored for the given sender - Optional previousMessageIdOption = - election.getMessageMap().entrySet().stream() - .filter(entry -> senderPk.equals(entry.getValue())) - .map(Map.Entry::getKey) - .findFirst(); - // Retrieve the creation time of the previous cast vote, if doesn't exist replace with min - // Value - long previousMessageCreation = - previousMessageIdOption - .map(messageRepo::getMessage) - .map(MessageGeneral::getData) - .map(CastVote.class::cast) - .map(CastVote::getCreation) - .orElse(Long.MIN_VALUE); + MessageID previousMessageId = election.getMessageMap().get(senderPk); + + // No previous message, we always handle it + if (previousMessageId == null) { + updateElectionWithVotes(castVote, messageId, senderPk, election); + return; + } + + // Retrieve previous message and make sure it is a CastVote + Data previousData = messageRepo.getMessage(previousMessageId).getData(); + if (previousData == null) { + throw new IllegalStateException( + "The message corresponding to " + messageId + " does not exist"); + } + + if (!(previousData instanceof CastVote)) { + throw new DataHandlingException( + previousData, "The previous message of a cast vote was not a CastVote"); + } + + CastVote previousCastVote = (CastVote) previousData; // Verify the current cast vote message is the last one received - if (previousMessageCreation <= castVote.getCreation()) { - List votes = castVote.getVotes(); - votes.sort(Comparator.comparing(Vote::getId)); - - Election updated = - election - .builder() - .updateMessageMap(senderPk, messageId) - .updateVotes(senderPk, votes) - .build(); - - electionRepository.updateElection(updated); + if (previousCastVote.getCreation() <= castVote.getCreation()) { + updateElectionWithVotes(castVote, messageId, senderPk, election); } } } + private void updateElectionWithVotes( + CastVote castVote, MessageID messageId, PublicKey senderPk, Election election) { + Election updated = + election + .builder() + .updateMessageMap(senderPk, messageId) + .updateVotes(senderPk, castVote.getVotes()) + .build(); + + electionRepository.updateElection(updated); + } + public static WitnessMessage electionSetupWitnessMessage(MessageID messageId, Election election) { WitnessMessage message = new WitnessMessage(messageId); message.setTitle("New Election Setup"); diff --git a/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/utility/handler/ElectionHandlerTest.java b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/utility/handler/ElectionHandlerTest.java index dc6d23b5e7..dd4a4440e4 100644 --- a/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/utility/handler/ElectionHandlerTest.java +++ b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/utility/handler/ElectionHandlerTest.java @@ -31,6 +31,7 @@ import java.security.GeneralSecurityException; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.reactivex.Completable; @@ -59,7 +60,7 @@ public class ElectionHandlerTest { private static final long CREATED_AT = CREATE_LAO.getCreation() + 10 * 1000; // 10 seconds later private static final long STARTED_AT = CREATE_LAO.getCreation() + 20 * 1000; // 20 seconds later private static final long OPENED_AT = CREATE_LAO.getCreation() + 30 * 1000; // 30 seconds later - private static final long END_AT = CREATE_LAO.getCreation() + 600 * 1000; // 60 seconds later + private static final long END_AT = CREATE_LAO.getCreation() + 60 * 1000; // 60 seconds later private static final String ELECTION_NAME = "Election Name"; private static final String ELECTION_ID = @@ -164,7 +165,7 @@ private void handleElectionOpen(Election election) messageHandler.handleMessage(messageSender, election.getChannel(), message); } - private MessageID handleCastVote(Vote vote, KeyPair senderKey) + private void handleCastVote(Vote vote, KeyPair senderKey) throws UnknownElectionException, UnknownRollCallException, UnknownLaoException, DataHandlingException, NoRollCallException { @@ -173,7 +174,6 @@ private MessageID handleCastVote(Vote vote, KeyPair senderKey) MessageGeneral message = new MessageGeneral(senderKey, castVote, gson); messageHandler.handleMessage(messageSender, OPEN_BALLOT_ELECTION.getChannel(), message); - return message.getMessageId(); } private void handleElectionEnd() @@ -293,15 +293,12 @@ public void castVoteWithOpenBallotScenario() handleElectionSetup(OPEN_BALLOT_ELECTION); handleElectionKey(OPEN_BALLOT_ELECTION, ELECTION_KEY); handleElectionOpen(OPEN_BALLOT_ELECTION); - MessageID vote1Id = handleCastVote(vote1, SENDER_KEY); - MessageID vote2Id = handleCastVote(vote2, ATTENDEE_KEY); + handleCastVote(vote1, SENDER_KEY); + handleCastVote(vote2, ATTENDEE_KEY); + // The expected hash is made on the sorted vote ids + String[] voteIds = Stream.of(vote1, vote2).map(Vote::getId).sorted().toArray(String[]::new); Election election = electionRepo.getElectionByChannel(OPEN_BALLOT_ELECTION.getChannel()); - // Sort the vote ids based on the message id - String[] voteIds = - vote1Id.getEncoded().compareTo(vote2Id.getEncoded()) < 0 - ? new String[] {vote1.getId(), vote2.getId()} - : new String[] {vote2.getId(), vote1.getId()}; assertEquals(Hash.hash(voteIds), election.computeRegisteredVotesHash()); } @@ -321,15 +318,12 @@ public void castVoteWithSecretBallotScenario() handleElectionKey(SECRET_BALLOT_ELECTION, encodedKey.getEncoded()); handleElectionOpen(SECRET_BALLOT_ELECTION); - MessageID vote1Id = handleCastVote(vote1, SENDER_KEY); - MessageID vote2Id = handleCastVote(vote2, ATTENDEE_KEY); + handleCastVote(vote1, SENDER_KEY); + handleCastVote(vote2, ATTENDEE_KEY); + // The expected hash is made on the sorted vote ids + String[] voteIds = Stream.of(vote1, vote2).map(Vote::getId).sorted().toArray(String[]::new); Election election = electionRepo.getElectionByChannel(OPEN_BALLOT_ELECTION.getChannel()); - // Sort the vote ids based on the message id - String[] voteIds = - vote1Id.getEncoded().compareTo(vote2Id.getEncoded()) < 0 - ? new String[] {vote1.getId(), vote2.getId()} - : new String[] {vote2.getId(), vote1.getId()}; assertEquals(Hash.hash(voteIds), election.computeRegisteredVotesHash()); } From 3ba6e8f1746703b05774205ffc4abfaa016b8d28 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Tue, 31 Jan 2023 23:05:40 +0100 Subject: [PATCH 059/121] Add support for removing reactions --- .../jsonrpc/messages/MessageRegistry.ts | 1 + .../src/core/network/validation/Validator.ts | 1 + .../network/validation/schemas/dataSchemas.ts | 2 + .../src/features/social/__tests__/utils.ts | 16 + .../features/social/components/ChirpCard.tsx | 49 ++- .../social/network/ReactionHandler.ts | 31 +- .../social/network/SocialMessageApi.ts | 18 +- .../__tests__/SocialMessageApi.test.ts | 33 ++ fe1-web/src/features/social/network/index.ts | 10 +- .../messages/reaction/DeleteReaction.ts | 48 +++ .../reaction/__tests__/DeleteReaction.test.ts | 74 ++++ .../social/network/messages/reaction/index.ts | 1 + .../features/social/reducer/SocialReducer.ts | 210 +++++++---- .../reducer/__tests__/SocialReducer.test.ts | 356 +++++++++--------- 14 files changed, 583 insertions(+), 267 deletions(-) create mode 100644 fe1-web/src/features/social/network/messages/reaction/DeleteReaction.ts create mode 100644 fe1-web/src/features/social/network/messages/reaction/__tests__/DeleteReaction.test.ts diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts index b2aa1d6584..6c9d264225 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts @@ -88,6 +88,7 @@ export class MessageRegistry { // Reactions [k(REACTION, ADD), { signature: POP_TOKEN }], + [k(REACTION, DELETE), { signature: POP_TOKEN }], // Coin [k(COIN, POST_TRANSACTION), { signature: KEYPAIR }], diff --git a/fe1-web/src/core/network/validation/Validator.ts b/fe1-web/src/core/network/validation/Validator.ts index 9a5588b9ea..fdecebd0d5 100644 --- a/fe1-web/src/core/network/validation/Validator.ts +++ b/fe1-web/src/core/network/validation/Validator.ts @@ -64,6 +64,7 @@ const schemaIds: Record> = { }, [ObjectType.REACTION]: { [ActionType.ADD]: 'dataAddReaction', + [ActionType.DELETE]: 'dataDeleteReaction', }, [ObjectType.COIN]: { [ActionType.POST_TRANSACTION]: 'postTransaction', diff --git a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts index cb7f711cdd..633cce0ad6 100644 --- a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts +++ b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts @@ -27,6 +27,7 @@ import dataDeleteChirp from 'protocol/query/method/message/data/dataDeleteChirp. import dataNotifyDeleteChirp from 'protocol/query/method/message/data/dataNotifyDeleteChirp.json'; import dataAddReaction from 'protocol/query/method/message/data/dataAddReaction.json'; +import dataDeleteReaction from 'protocol/query/method/message/data/dataDeleteReaction.json'; /* eslint-enable import/order */ const dataSchemas = [ @@ -57,6 +58,7 @@ const dataSchemas = [ dataNotifyDeleteChirp, dataAddReaction, + dataDeleteReaction, ]; export default dataSchemas; diff --git a/fe1-web/src/features/social/__tests__/utils.ts b/fe1-web/src/features/social/__tests__/utils.ts index 774c3f6ade..276ff6fa53 100644 --- a/fe1-web/src/features/social/__tests__/utils.ts +++ b/fe1-web/src/features/social/__tests__/utils.ts @@ -111,3 +111,19 @@ export const mockReaction4 = new Reaction({ chirpId: mockChirpId2, time: mockChirpTimestamp, }); + +export const mockReaction5 = new Reaction({ + id: new Hash('5555'), + sender: mockSender2, + codepoint: 'πŸ‘Ž', + chirpId: mockChirpId1, + time: mockChirpTimestamp, +}); + +export const mockReaction6 = new Reaction({ + id: new Hash('6666'), + sender: mockSender2, + codepoint: 'πŸ‘', + chirpId: mockChirpId3, + time: mockChirpTimestamp, +}); diff --git a/fe1-web/src/features/social/components/ChirpCard.tsx b/fe1-web/src/features/social/components/ChirpCard.tsx index 0d389df75b..70cd09afd1 100644 --- a/fe1-web/src/features/social/components/ChirpCard.tsx +++ b/fe1-web/src/features/social/components/ChirpCard.tsx @@ -21,14 +21,19 @@ import { SocialSearchParamList, SocialTopChirpsParamList, } from 'core/navigation/typing/social'; +import { Hash } from 'core/objects'; import { List, Spacing, Typography } from 'core/styles'; import STRINGS from 'resources/strings'; import { SocialMediaContext } from '../context'; import { SocialHooks } from '../hooks'; -import { requestAddReaction, requestDeleteChirp } from '../network/SocialMessageApi'; +import { + requestAddReaction, + requestDeleteChirp, + requestDeleteReaction, +} from '../network/SocialMessageApi'; import { Chirp } from '../objects'; -import { makeHasReactedSelector, makeReactionCountsSelector } from '../reducer'; +import { makeReactedSelector, makeReactionCountsSelector } from '../reducer'; type NavigationProps = CompositeScreenProps< CompositeScreenProps< @@ -106,20 +111,20 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { ); const reactions = useSelector(selectReactionList); - const selectHasReacted = useMemo( - () => makeHasReactedSelector(laoId, chirp.id, currentUserPopTokenPublicKey), + const selectReacted = useMemo( + () => makeReactedSelector(laoId, chirp.id, currentUserPopTokenPublicKey), [laoId, chirp.id, currentUserPopTokenPublicKey], ); - const hasReacted = useSelector(selectHasReacted); + const reacted = useSelector(selectReacted); const thumbsUp = reactions['πŸ‘']; const thumbsDown = reactions['πŸ‘Ž']; const heart = reactions['❀️']; const reactionsDisabled = { - 'πŸ‘': !isConnected || !currentUserPopTokenPublicKey || hasReacted['πŸ‘'], - 'πŸ‘Ž': !isConnected || !currentUserPopTokenPublicKey || hasReacted['πŸ‘Ž'], - '❀️': !isConnected || !currentUserPopTokenPublicKey || hasReacted['❀️'], + 'πŸ‘': !isConnected || !currentUserPopTokenPublicKey, + 'πŸ‘Ž': !isConnected || !currentUserPopTokenPublicKey, + '❀️': !isConnected || !currentUserPopTokenPublicKey, }; const showActionSheet = useActionSheet(); @@ -134,6 +139,16 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { }); }; + const deleteReaction = (reactionId: Hash) => { + requestDeleteReaction(reactionId, laoId).catch((err) => { + toast.show(`Could not delete reaction, error: ${err}`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + }); + }; + // TODO: delete a chirp posted with a PoP token from a previous roll call. const isSender = currentUserPopTokenPublicKey && @@ -195,10 +210,12 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { addReaction('πŸ‘')} + onPress={() => + reacted['πŸ‘'] ? deleteReaction(reacted['πŸ‘'].id) : addReaction('πŸ‘') + } disabled={reactionsDisabled['πŸ‘']} size="small" - buttonStyle="secondary" + buttonStyle={reacted['πŸ‘'] ? 'primary' : 'secondary'} toolbar /> @@ -209,10 +226,12 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { addReaction('πŸ‘Ž')} + onPress={() => + reacted['πŸ‘Ž'] ? deleteReaction(reacted['πŸ‘Ž'].id) : addReaction('πŸ‘Ž') + } disabled={reactionsDisabled['πŸ‘Ž']} size="small" - buttonStyle="secondary" + buttonStyle={reacted['πŸ‘Ž'] ? 'primary' : 'secondary'} toolbar /> @@ -223,10 +242,12 @@ const ChirpCard = ({ chirp, isFirstItem, isLastItem }: IPropTypes) => { addReaction('❀️')} + onPress={() => + reacted['❀️'] ? deleteReaction(reacted['❀️'].id) : addReaction('❀️') + } disabled={reactionsDisabled['❀️']} size="small" - buttonStyle="secondary" + buttonStyle={reacted['❀️'] ? 'primary' : 'secondary'} toolbar /> diff --git a/fe1-web/src/features/social/network/ReactionHandler.ts b/fe1-web/src/features/social/network/ReactionHandler.ts index 13ce0ed162..8f83f38dd6 100644 --- a/fe1-web/src/features/social/network/ReactionHandler.ts +++ b/fe1-web/src/features/social/network/ReactionHandler.ts @@ -3,8 +3,8 @@ import { dispatch } from 'core/redux'; import { SocialConfiguration } from '../interface'; import { Reaction } from '../objects'; -import { addReaction } from '../reducer'; -import { AddReaction } from './messages/reaction'; +import { addReaction, deleteReaction } from '../reducer'; +import { AddReaction, DeleteReaction } from './messages/reaction'; /** * Handler for social media chirp's reactions @@ -46,3 +46,30 @@ export const handleAddReactionMessage = dispatch(addReaction(laoId, reaction)); return true; }; + +/** + * Handles an deleteReaction message by deleting the respective reaction + */ +export const handleDeleteReactionMessage = + (getCurrentLaoId: SocialConfiguration['getCurrentLaoId']) => (msg: ProcessableMessage) => { + if ( + msg.messageData.object !== ObjectType.REACTION || + msg.messageData.action !== ActionType.DELETE + ) { + console.warn('handleDeleteReactionMessage was called to process an unsupported message'); + return false; + } + + const makeErr = (err: string) => `reaction/delete was not processed: ${err}`; + + const laoId = getCurrentLaoId(); + if (!laoId) { + console.warn(makeErr('no Lao is currently active')); + return false; + } + + const reactionMessage = msg.messageData as DeleteReaction; + + dispatch(deleteReaction(laoId, reactionMessage.reaction_id)); + return true; + }; diff --git a/fe1-web/src/features/social/network/SocialMessageApi.ts b/fe1-web/src/features/social/network/SocialMessageApi.ts index 70e6f998ac..99a3359a0a 100644 --- a/fe1-web/src/features/social/network/SocialMessageApi.ts +++ b/fe1-web/src/features/social/network/SocialMessageApi.ts @@ -2,7 +2,7 @@ import { publish } from 'core/network'; import { getReactionChannel, getUserSocialChannel, Hash, PublicKey, Timestamp } from 'core/objects'; import { AddChirp, DeleteChirp } from './messages/chirp'; -import { AddReaction } from './messages/reaction'; +import { AddReaction, DeleteReaction } from './messages/reaction'; /** * Contains all functions to send social media related messages. @@ -77,3 +77,19 @@ export function requestAddReaction( return publish(getReactionChannel(laoId), message); } + +/** + * Sends a query to the server to add a new reaction. + * + * @param reaction_codepoint - The codepoint corresponding to the reaction type + * @param chirp_id - The id of the chirp where the reaction is added + * @param laoId - The id of the Lao in which to add a reaction + */ +export function requestDeleteReaction(reactionId: Hash, laoId: Hash): Promise { + const message = new DeleteReaction({ + reaction_id: reactionId, + timestamp: Timestamp.EpochNow(), + }); + + return publish(getReactionChannel(laoId), message); +} diff --git a/fe1-web/src/features/social/network/__tests__/SocialMessageApi.test.ts b/fe1-web/src/features/social/network/__tests__/SocialMessageApi.test.ts index e56d1dc004..6c2d321d44 100644 --- a/fe1-web/src/features/social/network/__tests__/SocialMessageApi.test.ts +++ b/fe1-web/src/features/social/network/__tests__/SocialMessageApi.test.ts @@ -8,8 +8,10 @@ import { ActionType, MessageData, ObjectType } from 'core/network/jsonrpc/messag import { publish as mockPublish } from 'core/network/JsonRpcApi'; import { Hash, PublicKey } from 'core/objects'; import { OpenedLaoStore } from 'features/lao/store'; +import { mockChirp0, mockReaction1 } from 'features/social/__tests__/utils'; import { AddChirp } from '../messages/chirp'; +import { AddReaction, DeleteReaction } from '../messages/reaction'; import * as msApi from '../SocialMessageApi'; jest.mock('core/network/JsonRpcApi'); @@ -61,4 +63,35 @@ describe('MessageApi', () => { expect(channel).toBe(`/root/${serializedMockLaoId}/social/${testKeyPair.publicKey}`); checkDataAddChirp(msgData); }); + + it('should create the correct request for requestAddReaction', async () => { + await msApi.requestAddReaction(mockReaction1.codepoint, mockChirp0.id, mockLaoId); + + expect(publishMock).toBeCalledTimes(1); + const [channel, msgData] = publishMock.mock.calls[0]; + expect(channel).toBe(`/root/${serializedMockLaoId}/social/reactions`); + expect(msgData).toBeInstanceOf(AddReaction); + expect(msgData).toEqual({ + object: ObjectType.REACTION, + action: ActionType.ADD, + reaction_codepoint: mockReaction1.codepoint, + chirp_id: mockChirp0.id, + timestamp: expect.anything(), + }); + }); + + it('should create the correct request for requestDeleteReaction', async () => { + await msApi.requestDeleteReaction(mockReaction1.id, mockLaoId); + + expect(publishMock).toBeCalledTimes(1); + const [channel, msgData] = publishMock.mock.calls[0]; + expect(channel).toBe(`/root/${serializedMockLaoId}/social/reactions`); + expect(msgData).toBeInstanceOf(DeleteReaction); + expect(msgData).toEqual({ + object: ObjectType.REACTION, + action: ActionType.DELETE, + reaction_id: mockReaction1.id, + timestamp: expect.anything(), + }); + }); }); diff --git a/fe1-web/src/features/social/network/index.ts b/fe1-web/src/features/social/network/index.ts index cf9011a61c..6f47bfa33f 100644 --- a/fe1-web/src/features/social/network/index.ts +++ b/fe1-web/src/features/social/network/index.ts @@ -8,8 +8,8 @@ import { handleNotifyDeleteChirpMessage, } from './ChirpHandler'; import { AddChirp, DeleteChirp, NotifyAddChirp, NotifyDeleteChirp } from './messages/chirp'; -import { AddReaction } from './messages/reaction'; -import { handleAddReactionMessage } from './ReactionHandler'; +import { AddReaction, DeleteReaction } from './messages/reaction'; +import { handleAddReactionMessage, handleDeleteReactionMessage } from './ReactionHandler'; /** * Configures the network callbacks in a MessageRegistry. @@ -50,4 +50,10 @@ export function configureNetwork(configuration: SocialConfiguration) { handleAddReactionMessage(configuration.getCurrentLaoId), AddReaction.fromJson, ); + configuration.messageRegistry.add( + ObjectType.REACTION, + ActionType.DELETE, + handleDeleteReactionMessage(configuration.getCurrentLaoId), + DeleteReaction.fromJson, + ); } diff --git a/fe1-web/src/features/social/network/messages/reaction/DeleteReaction.ts b/fe1-web/src/features/social/network/messages/reaction/DeleteReaction.ts new file mode 100644 index 0000000000..27aee18a48 --- /dev/null +++ b/fe1-web/src/features/social/network/messages/reaction/DeleteReaction.ts @@ -0,0 +1,48 @@ +import { ActionType, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; +import { validateDataObject } from 'core/network/validation'; +import { Hash, ProtocolError, Timestamp } from 'core/objects'; +import { MessageDataProperties } from 'core/types'; + +/** Data sent to add a reaction */ +export class DeleteReaction implements MessageData { + public readonly object: ObjectType = ObjectType.REACTION; + + public readonly action: ActionType = ActionType.DELETE; + + // message_id of the add reaction message + public readonly reaction_id: Hash; + + // timestamp of this reaction deletion request + public readonly timestamp: Timestamp; + + constructor(msg: MessageDataProperties) { + if (!msg.reaction_id) { + throw new ProtocolError( + "Undefined 'reaction_id' parameter encountered during 'DeleteReaction'", + ); + } + this.reaction_id = msg.reaction_id; + + if (!msg.timestamp) { + throw new ProtocolError("Undefined 'timestamp' parameter encountered during 'AddReaction'"); + } + this.timestamp = msg.timestamp; + } + + /** + * Creates an AddReaction object from a given object + * @param obj + */ + public static fromJson(obj: any): DeleteReaction { + const { errors } = validateDataObject(ObjectType.REACTION, ActionType.DELETE, obj); + + if (errors !== null) { + throw new ProtocolError(`Invalid reaction add\n\n${errors}`); + } + + return new DeleteReaction({ + reaction_id: new Hash(obj.reaction_id), + timestamp: new Timestamp(obj.timestamp), + }); + } +} diff --git a/fe1-web/src/features/social/network/messages/reaction/__tests__/DeleteReaction.test.ts b/fe1-web/src/features/social/network/messages/reaction/__tests__/DeleteReaction.test.ts new file mode 100644 index 0000000000..8d6e10650c --- /dev/null +++ b/fe1-web/src/features/social/network/messages/reaction/__tests__/DeleteReaction.test.ts @@ -0,0 +1,74 @@ +import 'jest-extended'; +import '__tests__/utils/matchers'; + +import { ActionType, ObjectType } from 'core/network/jsonrpc/messages'; +import { Base64UrlData, Hash, ProtocolError, Timestamp } from 'core/objects'; + +import { DeleteReaction } from '../DeleteReaction'; + +const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 +const mockReactionId = Base64UrlData.encode('reaction_id'); +const ID = new Hash(mockReactionId.toString()); + +const sampleDeleteReaction = { + object: ObjectType.REACTION, + action: ActionType.DELETE, + reaction_id: ID, + timestamp: TIMESTAMP, +}; + +const dataDeleteReaction = `{ + "object": "${ObjectType.REACTION}", + "action": "${ActionType.DELETE}", + "reaction_id": "${ID.toState()}", + "timestamp": ${TIMESTAMP.toState()} + }`; + +describe('DeleteReaction', () => { + it('should be created correctly from JSON', () => { + expect(new DeleteReaction(sampleDeleteReaction)).toBeJsonEqual(sampleDeleteReaction); + const temp = { + object: ObjectType.REACTION, + action: ActionType.DELETE, + reaction_id: ID, + timestamp: TIMESTAMP, + }; + expect(new DeleteReaction(temp)).toBeJsonEqual(temp); + }); + + it('should be parsed correctly from JSON', () => { + const obj = JSON.parse(dataDeleteReaction); + expect(DeleteReaction.fromJson(obj)).toBeJsonEqual(sampleDeleteReaction); + }); + + it('fromJson should throw an error if the Json has incorrect action', () => { + const obj = { + object: ObjectType.REACTION, + action: ActionType.NOTIFY_ADD, + reaction_id: ID, + timestamp: TIMESTAMP, + }; + const createFromJson = () => DeleteReaction.fromJson(obj); + expect(createFromJson).toThrow(ProtocolError); + }); + + describe('constructor', () => { + it('should throw an error if reaction_id is undefined', () => { + const wrongObj = () => + new DeleteReaction({ + reaction_id: undefined as unknown as Hash, + timestamp: TIMESTAMP, + }); + expect(wrongObj).toThrow(ProtocolError); + }); + + it('should throw an error is timestamp is undefined', () => { + const wrongObj = () => + new DeleteReaction({ + reaction_id: ID, + timestamp: undefined as unknown as Timestamp, + }); + expect(wrongObj).toThrow(ProtocolError); + }); + }); +}); diff --git a/fe1-web/src/features/social/network/messages/reaction/index.ts b/fe1-web/src/features/social/network/messages/reaction/index.ts index c4da507118..67de6263c6 100644 --- a/fe1-web/src/features/social/network/messages/reaction/index.ts +++ b/fe1-web/src/features/social/network/messages/reaction/index.ts @@ -1 +1,2 @@ export * from './AddReaction'; +export * from './DeleteReaction'; diff --git a/fe1-web/src/features/social/reducer/SocialReducer.ts b/fe1-web/src/features/social/reducer/SocialReducer.ts index af5a22fd9f..67980db492 100644 --- a/fe1-web/src/features/social/reducer/SocialReducer.ts +++ b/fe1-web/src/features/social/reducer/SocialReducer.ts @@ -21,8 +21,13 @@ interface SocialReducerState { byId: Record; // maps a sender to the list of ChirpIds he sent byUser: Record; - // maps a chirpId to the pair of the reaction_codepoint and the list of userPublicKeys - reactionsByChirp: Record>; + // stores the score for each chirp id + scoreByChirpId: Record; + + // mapping of chirp ids to list of reactions + reactionsByChirpId: Record; + // maps a reactionId to its ReactionState + reactionsById: Record; } // Root state for the Social Reducer @@ -31,15 +36,19 @@ export interface SocialLaoReducerState { byLaoId: Record; } +const getEmptyByLaoState = (): SocialLaoReducerState['byLaoId'][''] => ({ + allIdsInOrder: [], + byId: {}, + byUser: {}, + scoreByChirpId: {}, + reactionsByChirpId: {}, + reactionsById: {}, +}); + +export const SCORE_BY_CODE_POINT: Record = { 'πŸ‘': 1, 'πŸ‘Ž': -1, '❀️': 1 }; + const initialState: SocialLaoReducerState = { - byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, - }, + byLaoId: {}, }; /* Name of the social media slice in storage */ @@ -85,12 +94,7 @@ const socialSlice = createSlice({ const { laoId, chirp } = action.payload; if (!(laoId in state.byLaoId)) { - state.byLaoId[laoId] = { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }; + state.byLaoId[laoId] = getEmptyByLaoState(); } const store = state.byLaoId[laoId]; @@ -107,6 +111,9 @@ const socialSlice = createSlice({ const insertIdxInAll = findInsertIdx(store.allIdsInOrder, store.byId, chirp.time); store.allIdsInOrder.splice(insertIdxInAll, 0, chirp.id); + // initialize the score at 0 + store.scoreByChirpId[chirp.id] = 0; + if (!state.byLaoId[laoId].byUser[chirp.sender]) { store.byUser[chirp.sender] = [chirp.id]; } else { @@ -137,12 +144,7 @@ const socialSlice = createSlice({ const { laoId, chirp } = action.payload; if (!(laoId in state.byLaoId)) { - state.byLaoId[laoId] = { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }; + state.byLaoId[laoId] = getEmptyByLaoState(); } const store = state.byLaoId[laoId]; @@ -186,33 +188,92 @@ const socialSlice = createSlice({ const { laoId, reaction } = action.payload; if (!(laoId in state.byLaoId)) { - state.byLaoId[laoId] = { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }; + state.byLaoId[laoId] = getEmptyByLaoState(); } const store = state.byLaoId[laoId]; - if (!store.reactionsByChirp[reaction.chirpId]) { - store.reactionsByChirp[reaction.chirpId] = { [reaction.codepoint]: [reaction.sender] }; - } else if (!store.reactionsByChirp[reaction.chirpId][reaction.codepoint]) { - store.reactionsByChirp[reaction.chirpId][reaction.codepoint] = [reaction.sender]; - } else if ( - !store.reactionsByChirp[reaction.chirpId][reaction.codepoint].includes(reaction.sender) - ) { - store.reactionsByChirp[reaction.chirpId][reaction.codepoint].push(reaction.sender); + // add reaction to list of reactions for chirp + if (!store.reactionsByChirpId[reaction.chirpId]) { + store.reactionsByChirpId[reaction.chirpId] = [reaction.id]; } else { - console.debug('The sender already reacted to this reaction'); + // check if the user already reacted with this codepoint + const duplicateEntry = store.reactionsByChirpId[reaction.chirpId].find((reactionId) => { + const storedReaction = store.reactionsById[reactionId]; + + return ( + storedReaction.sender === reaction.sender && + storedReaction.codepoint === reaction.codepoint + ); + }); + + if (duplicateEntry) { + console.debug('The sender already reacted to this reaction'); + return; + } + + // if not, store it + store.reactionsByChirpId[reaction.chirpId].push(reaction.id); } + + // store reaction itself + store.reactionsById[reaction.id] = reaction; + store.scoreByChirpId[reaction.chirpId] = + // previous score if there is one + (store.scoreByChirpId[reaction.chirpId] || 0) + + // difference for codepoint if there is one + (SCORE_BY_CODE_POINT[reaction.codepoint] || 0); + }, + }, + + // Remove reactions from chirp + deleteReaction: { + prepare(laoId: Hash, reactionId: Hash) { + return { + payload: { + laoId: laoId.valueOf(), + reactionId: reactionId.toState(), + }, + }; + }, + reducer( + state, + action: PayloadAction<{ + laoId: string; + reactionId: string; + }>, + ) { + const { laoId, reactionId } = action.payload; + + if (!(laoId in state.byLaoId)) { + state.byLaoId[laoId] = getEmptyByLaoState(); + } + + const store = state.byLaoId[laoId]; + + const reaction = store.reactionsById[reactionId]; + + // delete id from mapping, works even if it does not exist + delete store.reactionsById[reactionId]; + + if (!store.reactionsByChirpId[reaction.chirpId]) { + // no need to remove something that does not exist + return; + } + + // filter out reaction from array + store.reactionsByChirpId[reaction.chirpId] = store.reactionsByChirpId[ + reaction.chirpId + ].filter((rId) => rId !== reaction.id); + + // and reset score for chirp + store.scoreByChirpId[reaction.chirpId] -= SCORE_BY_CODE_POINT[reaction.codepoint] || 0; }, }, }, }); -export const { addChirp, deleteChirp, addReaction } = socialSlice.actions; +export const { addChirp, deleteChirp, addReaction, deleteReaction } = socialSlice.actions; export const socialReduce = socialSlice.reducer; @@ -282,7 +343,7 @@ export const makeReactionCountsSelector = (laoId: Hash, chirpId: Hash) => const serializedChirpId = chirpId.toState(); const byLaoId = state.byLaoId[serializedLaoId]; - if (!byLaoId || !byLaoId.reactionsByChirp[serializedChirpId]) { + if (!byLaoId || !byLaoId.reactionsByChirpId[serializedChirpId]) { // no reactions so far return { 'πŸ‘': 0, @@ -291,38 +352,58 @@ export const makeReactionCountsSelector = (laoId: Hash, chirpId: Hash) => }; } - const byChirpId = byLaoId.reactionsByChirp[serializedChirpId]; + // all reactions for 'serializedChirpId' + const reactions = byLaoId.reactionsByChirpId[serializedChirpId].map( + (reactionId) => byLaoId.reactionsById[reactionId], + ); - return { - 'πŸ‘': byChirpId['πŸ‘'] ? byChirpId['πŸ‘'].length : 0, - 'πŸ‘Ž': byChirpId['πŸ‘Ž'] ? byChirpId['πŸ‘Ž'].length : 0, - '❀️': byChirpId['❀️'] ? byChirpId['❀️'].length : 0, - }; + // count them by codepoint + const reactionCodePointCounts = reactions.reduce>( + (counts, reaction) => { + if (counts[reaction.codepoint]) { + counts[reaction.codepoint] += 1; + } else { + counts[reaction.codepoint] = 1; + } + + return counts; + }, + { 'πŸ‘': 0, 'πŸ‘Ž': 0, '❀️': 0 }, + ); + + return reactionCodePointCounts; }); -export const makeHasReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey) => - createSelector(selectSocialState, (state: SocialLaoReducerState): Record => { +export const makeReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey) => + createSelector(selectSocialState, (state: SocialLaoReducerState): Record => { const serializedLaoId = laoId.toState(); const serializedChirpId = chirpId.toState(); const serializedPublicKey = user?.toState(); const byLaoId = state.byLaoId[serializedLaoId]; - if (!serializedPublicKey || !byLaoId || !byLaoId.reactionsByChirp[serializedChirpId]) { + if (!serializedPublicKey || !byLaoId || !byLaoId.reactionsByChirpId[serializedChirpId]) { // no reactions so far - return { - 'πŸ‘': false, - 'πŸ‘Ž': false, - '❀️': false, - }; + return {}; } - const byChirpId = byLaoId.reactionsByChirp[serializedChirpId]; + // all reactions for 'serializedChirpId' + const reactions = byLaoId.reactionsByChirpId[serializedChirpId].map( + (reactionId) => byLaoId.reactionsById[reactionId], + ); - return { - 'πŸ‘': byChirpId['πŸ‘'] ? byChirpId['πŸ‘'].includes(serializedPublicKey) : false, - 'πŸ‘Ž': byChirpId['πŸ‘Ž'] ? byChirpId['πŸ‘Ž'].includes(serializedPublicKey) : false, - '❀️': byChirpId['❀️'] ? byChirpId['❀️'].includes(serializedPublicKey) : false, - }; + // add reaction mapping for each code point if the user matches + const reactionByCodepoints = reactions.reduce>((obj, reaction) => { + if (reaction.sender !== serializedPublicKey) { + // skip reactions by other suers + return obj; + } + + obj[reaction.codepoint] = Reaction.fromState(reaction); + + return obj; + }, {}); + + return reactionByCodepoints; }); export const makeTopChirpsSelector = (laoId: Hash, max: number) => @@ -334,16 +415,7 @@ export const makeTopChirpsSelector = (laoId: Hash, max: number) => return []; } - const scorePerId = byLaoId.allIdsInOrder - .map<[string, number]>((chirpId) => { - const byChirpId = byLaoId.reactionsByChirp[chirpId] || {}; - const score = - (byChirpId['πŸ‘']?.length || 0) + - (byChirpId['❀️']?.length || 0) - - (byChirpId['πŸ‘Ž']?.length || 0); - - return [chirpId, score]; - }) + const scorePerId = Object.entries(byLaoId.scoreByChirpId) // filter deleted chirps .filter((tuple) => !byLaoId.byId[tuple[0]].isDeleted); diff --git a/fe1-web/src/features/social/reducer/__tests__/SocialReducer.test.ts b/fe1-web/src/features/social/reducer/__tests__/SocialReducer.test.ts index 864ab94df6..eaf66336a8 100644 --- a/fe1-web/src/features/social/reducer/__tests__/SocialReducer.test.ts +++ b/fe1-web/src/features/social/reducer/__tests__/SocialReducer.test.ts @@ -1,4 +1,5 @@ import 'jest-extended'; +import '__tests__/utils/matchers'; import { describe } from '@jest/globals'; import { AnyAction } from 'redux'; @@ -22,8 +23,9 @@ import { mockReaction2, mockReaction3, mockReaction4, + mockReaction5, + mockReaction6, mockSender1, - mockSender2, } from 'features/social/__tests__/utils'; import { @@ -32,9 +34,10 @@ import { deleteChirp, makeChirpsList, makeChirpsListOfUser, - makeHasReactedSelector, + makeReactedSelector, makeReactionCountsSelector, makeTopChirpsSelector, + SCORE_BY_CODE_POINT, SocialLaoReducerState, socialReduce, } from '../SocialReducer'; @@ -42,75 +45,50 @@ import { // region test data const emptyState: SocialLaoReducerState = { - byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, - }, + byLaoId: {}, }; const chirpFilledState0Deleted: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [], byId: { [mockChirpId0.toState()]: mockChirp0DeletedFake.toState() }, byUser: {}, - reactionsByChirp: {}, + scoreByChirpId: {}, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState0Added: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp0.id.toState()], byId: { [mockChirp0.id.toState()]: mockChirp0.toState() }, byUser: { [mockChirp0.sender.toState()]: [mockChirp0.id.toState()] }, - reactionsByChirp: {}, + scoreByChirpId: { [mockChirp0.id.toState()]: 0 }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState1: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp1.id.toState()], byId: { [mockChirp1.id.toState()]: mockChirp1.toState() }, byUser: { [mockChirp1.sender.toState()]: [mockChirp1.id.toState()] }, - reactionsByChirp: {}, + scoreByChirpId: { [mockChirp1.id.toState()]: 0 }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState2: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp2.id.toState(), mockChirp1.id.toState()], byId: { @@ -121,19 +99,18 @@ const chirpFilledState2: SocialLaoReducerState = { [mockChirp1.sender.toState()]: [mockChirp1.id.toState()], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: {}, + scoreByChirpId: { + [mockChirp1.id.toState()]: 0, + [mockChirp2.id.toState()]: 0, + }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState3: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp2.id.toState(), mockChirp3.id.toState(), mockChirp1.id.toState()], byId: { @@ -145,19 +122,19 @@ const chirpFilledState3: SocialLaoReducerState = { [mockChirp1.sender.toState()]: [mockChirp3.id.toState(), mockChirp1.id.toState()], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: {}, + scoreByChirpId: { + [mockChirp1.id.toState()]: 0, + [mockChirp2.id.toState()]: 0, + [mockChirp3.id.toState()]: 0, + }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState4: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [ mockChirp4.id.toState(), @@ -179,19 +156,20 @@ const chirpFilledState4: SocialLaoReducerState = { ], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: {}, + scoreByChirpId: { + [mockChirp1.id.toState()]: 0, + [mockChirp2.id.toState()]: 0, + [mockChirp3.id.toState()]: 0, + [mockChirp4.id.toState()]: 0, + }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState4Chirp1Deleted: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [ mockChirp4.id.toState(), @@ -213,19 +191,20 @@ const chirpFilledState4Chirp1Deleted: SocialLaoReducerState = { ], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: {}, + scoreByChirpId: { + [mockChirp1.id.toState()]: 0, + [mockChirp2.id.toState()]: 0, + [mockChirp3.id.toState()]: 0, + [mockChirp4.id.toState()]: 0, + }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const chirpFilledState4Chirp4Deleted: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [ mockChirp4.id.toState(), @@ -247,62 +226,61 @@ const chirpFilledState4Chirp4Deleted: SocialLaoReducerState = { ], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: {}, + scoreByChirpId: { + [mockChirp1.id.toState()]: 0, + [mockChirp2.id.toState()]: 0, + [mockChirp3.id.toState()]: 0, + [mockChirp4.id.toState()]: 0, + }, + reactionsByChirpId: {}, + reactionsById: {}, }, }, }; const reactionFilledState1: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [], byId: {}, byUser: {}, - reactionsByChirp: { [mockChirpId1.toString()]: { 'πŸ‘': [mockSender1.toString()] } }, + scoreByChirpId: { [mockChirpId1.toState()]: SCORE_BY_CODE_POINT[mockReaction1.codepoint] }, + reactionsByChirpId: { [mockChirpId1.toState()]: [mockReaction1.id.toState()] }, + reactionsById: { [mockReaction1.id.toState()]: mockReaction1.toState() }, }, }, }; const reactionFilledState11: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp1.id.toState()], byId: { [mockChirp1.id.toState()]: mockChirp1.toState() }, byUser: { [mockChirp1.sender.toState()]: [mockChirp1.id.toState()] }, - reactionsByChirp: { [mockChirpId1.toState()]: { 'πŸ‘': [mockSender1.toState()] } }, + scoreByChirpId: { [mockChirpId1.toState()]: SCORE_BY_CODE_POINT[mockReaction1.codepoint] }, + reactionsByChirpId: { [mockChirpId1.toState()]: [mockReaction1.id.toState()] }, + reactionsById: { [mockReaction1.id.toState()]: mockReaction1.toState() }, }, }, }; const reactionFilledState2: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [], byId: {}, byUser: {}, - reactionsByChirp: { - [mockChirpId1.toState()]: { - 'πŸ‘': [mockSender1.toState()], - '❀️': [mockSender1.toState()], - }, + scoreByChirpId: { + [mockChirpId1.toState()]: + SCORE_BY_CODE_POINT[mockReaction1.codepoint] + + SCORE_BY_CODE_POINT[mockReaction2.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [mockReaction1.id.toState(), mockReaction2.id.toState()], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction2.id.toState()]: mockReaction2.toState(), }, }, }, @@ -310,21 +288,21 @@ const reactionFilledState2: SocialLaoReducerState = { const reactionFilledState22: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp1.id.toState()], byId: { [mockChirp1.id.toState()]: mockChirp1.toState() }, byUser: { [mockChirp1.sender.toState()]: [mockChirp1.id.toState()] }, - reactionsByChirp: { - [mockChirpId1.toState()]: { - 'πŸ‘': [mockSender1.toState()], - '❀️': [mockSender1.toState()], - }, + scoreByChirpId: { + [mockChirpId1.toState()]: + SCORE_BY_CODE_POINT[mockReaction1.codepoint] + + SCORE_BY_CODE_POINT[mockReaction2.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [mockReaction1.id.toState(), mockReaction2.id.toState()], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction2.id.toState()]: mockReaction2.toState(), }, }, }, @@ -332,21 +310,27 @@ const reactionFilledState22: SocialLaoReducerState = { const reactionFilledState3: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [], byId: {}, byUser: {}, - reactionsByChirp: { - [mockChirpId1.toState()]: { - 'πŸ‘': [mockSender1.toState(), mockSender2.toState()], - '❀️': [mockSender1.toState()], - }, + scoreByChirpId: { + [mockChirpId1.toState()]: + SCORE_BY_CODE_POINT[mockReaction1.codepoint] + + SCORE_BY_CODE_POINT[mockReaction2.codepoint] + + SCORE_BY_CODE_POINT[mockReaction3.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [ + mockReaction1.id.toState(), + mockReaction2.id.toState(), + mockReaction3.id.toState(), + ], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction2.id.toState()]: mockReaction2.toState(), + [mockReaction3.id.toState()]: mockReaction3.toState(), }, }, }, @@ -354,21 +338,27 @@ const reactionFilledState3: SocialLaoReducerState = { const reactionFilledState33: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [mockChirp1.id.toState()], byId: { [mockChirp1.id.toState()]: mockChirp1.toState() }, byUser: { [mockChirp1.sender.toState()]: [mockChirp1.id.toState()] }, - reactionsByChirp: { - [mockChirpId1.toState()]: { - 'πŸ‘': [mockSender1.toState(), mockSender2.toState()], - '❀️': [mockSender1.toState()], - }, + scoreByChirpId: { + [mockChirpId1.toState()]: + SCORE_BY_CODE_POINT[mockReaction1.codepoint] + + SCORE_BY_CODE_POINT[mockReaction2.codepoint] + + SCORE_BY_CODE_POINT[mockReaction3.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [ + mockReaction1.id.toState(), + mockReaction2.id.toState(), + mockReaction3.id.toState(), + ], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction2.id.toState()]: mockReaction2.toState(), + [mockReaction3.id.toState()]: mockReaction3.toState(), }, }, }, @@ -376,34 +366,30 @@ const reactionFilledState33: SocialLaoReducerState = { const reactionFilledState4: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { allIdsInOrder: [], byId: {}, byUser: {}, - reactionsByChirp: { - [mockChirpId1.toState()]: { 'πŸ‘': [mockSender1.toState()] }, - [mockChirpId2.toState()]: { 'πŸ‘': [mockSender2.toState()] }, + scoreByChirpId: { + [mockChirpId1.toState()]: SCORE_BY_CODE_POINT[mockReaction1.codepoint], + [mockChirpId2.toState()]: SCORE_BY_CODE_POINT[mockReaction4.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [mockReaction1.id.toState()], + [mockChirpId2.toState()]: [mockReaction4.id.toState()], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction4.id.toState()]: mockReaction4.toState(), }, }, }, }; -const reactionFilledState44: SocialLaoReducerState = { +const reactionFilledState5: SocialLaoReducerState = { byLaoId: { - myLaoId: { - allIdsInOrder: [], - byId: {}, - byUser: {}, - reactionsByChirp: {}, - }, [serializedMockLaoId]: { - allIdsInOrder: [mockChirp2.id.toState(), mockChirp1.id.toState()], + allIdsInOrder: [mockChirp1.id.toState(), mockChirp2.id.toState()], byId: { [mockChirp1.id.toState()]: mockChirp1.toState(), [mockChirp2.id.toState()]: mockChirp2.toState(), @@ -413,10 +399,23 @@ const reactionFilledState44: SocialLaoReducerState = { [mockChirp1.sender.toState()]: [mockChirp1.id.toState(), mockChirp3.id.toState()], [mockChirp2.sender.toState()]: [mockChirp2.id.toState()], }, - reactionsByChirp: { - [mockChirpId1.toState()]: { 'πŸ‘': [mockSender1.toState()], 'πŸ‘Ž': [mockSender2.toState()] }, - [mockChirpId2.toState()]: { 'πŸ‘': [mockSender2.toState()] }, - [mockChirpId3.toState()]: { 'πŸ‘': [mockSender2.toState()] }, + scoreByChirpId: { + [mockChirpId1.toState()]: + SCORE_BY_CODE_POINT[mockReaction1.codepoint] + + SCORE_BY_CODE_POINT[mockReaction5.codepoint], + [mockChirpId2.toState()]: SCORE_BY_CODE_POINT[mockReaction4.codepoint], + [mockChirpId3.toState()]: SCORE_BY_CODE_POINT[mockReaction6.codepoint], + }, + reactionsByChirpId: { + [mockChirpId1.toState()]: [mockReaction1.id.toState(), mockReaction5.id.toState()], + [mockChirpId2.toState()]: [mockReaction4.id.toState()], + [mockChirpId3.toState()]: [mockReaction6.id.toState()], + }, + reactionsById: { + [mockReaction1.id.toState()]: mockReaction1.toState(), + [mockReaction4.id.toState()]: mockReaction4.toState(), + [mockReaction5.id.toState()]: mockReaction5.toState(), + [mockReaction6.id.toState()]: mockReaction6.toState(), }, }, }, @@ -541,33 +540,42 @@ describe('SocialReducer', () => { describe('reaction reducer', () => { it('should create entry for a chirp when receiving the first reaction on it', () => { - expect(socialReduce(emptyState, addReaction(mockLaoId, mockReaction1))).toEqual( + console.error(emptyState); + console.error( + socialReduce( + { + byLaoId: {}, + }, + addReaction(mockLaoId, mockReaction1), + ).byLaoId[mockLaoId.toState()].allIdsInOrder, + ); + expect(socialReduce(emptyState, addReaction(mockLaoId, mockReaction1))).toBeJsonEqual( reactionFilledState1, ); }); it('should add reaction codepoint to an existing chirp', () => { - expect(socialReduce(reactionFilledState1, addReaction(mockLaoId, mockReaction2))).toEqual( - reactionFilledState2, - ); + expect( + socialReduce(reactionFilledState1, addReaction(mockLaoId, mockReaction2)), + ).toBeJsonEqual(reactionFilledState2); }); it('should add new reaction sender for a chirp', () => { - expect(socialReduce(reactionFilledState2, addReaction(mockLaoId, mockReaction3))).toEqual( - reactionFilledState3, - ); + expect( + socialReduce(reactionFilledState2, addReaction(mockLaoId, mockReaction3)), + ).toBeJsonEqual(reactionFilledState3); }); it('should not add existing sender of a reaction for a chirp', () => { - expect(socialReduce(reactionFilledState3, addReaction(mockLaoId, mockReaction1))).toEqual( - reactionFilledState3, - ); + expect( + socialReduce(reactionFilledState3, addReaction(mockLaoId, mockReaction1)), + ).toBeJsonEqual(reactionFilledState3); }); it('should create new chirp entry correctly', () => { - expect(socialReduce(reactionFilledState1, addReaction(mockLaoId, mockReaction4))).toEqual( - reactionFilledState4, - ); + expect( + socialReduce(reactionFilledState1, addReaction(mockLaoId, mockReaction4)), + ).toBeJsonEqual(reactionFilledState4); }); }); @@ -618,33 +626,23 @@ describe('SocialReducer', () => { }); describe('makeHasReactedSelector', () => { - it('should return false for an empty state', () => { + it('should return an empty object for an empty state', () => { expect( - makeHasReactedSelector(mockLaoId, mockChirpId1, mockSender1).resultFunc(emptyState), - ).toEqual({ - 'πŸ‘': false, - 'πŸ‘Ž': false, - '❀️': false, - }); + makeReactedSelector(mockLaoId, mockChirpId1, mockSender1).resultFunc(emptyState), + ).toEqual({}); }); - it('should return false for non-stored chirp', () => { + it('should return empty object for chirp without reactions', () => { expect( - makeHasReactedSelector(mockLaoId, mockChirpId2, mockSender1).resultFunc( - reactionFilledState1, - ), - ).toEqual({ 'πŸ‘': false, 'πŸ‘Ž': false, '❀️': false }); + makeReactedSelector(mockLaoId, mockChirpId2, mockSender1).resultFunc(reactionFilledState1), + ).toEqual({}); }); it('should return the reacted state correctly', () => { expect( - makeHasReactedSelector(mockLaoId, mockChirpId1, mockSender1).resultFunc( - reactionFilledState11, - ), + makeReactedSelector(mockLaoId, mockChirpId1, mockSender1).resultFunc(reactionFilledState11), ).toEqual({ - 'πŸ‘': true, - 'πŸ‘Ž': false, - '❀️': false, + 'πŸ‘': mockReaction1, }); }); }); @@ -655,20 +653,20 @@ describe('SocialReducer', () => { }); it('should return the chirps in the correct order', () => { - expect(makeTopChirpsSelector(mockLaoId, 2).resultFunc(reactionFilledState44)).toEqual([ + expect(makeTopChirpsSelector(mockLaoId, 2).resultFunc(reactionFilledState5)).toEqual([ mockChirp2, mockChirp1, ]); }); it("should return at most 'max' chirps", () => { - expect(makeTopChirpsSelector(mockLaoId, 1).resultFunc(reactionFilledState44)).toEqual([ + expect(makeTopChirpsSelector(mockLaoId, 1).resultFunc(reactionFilledState5)).toEqual([ mockChirp2, ]); }); it('should omit deleted chirps', () => { - expect(makeTopChirpsSelector(mockLaoId, 3).resultFunc(reactionFilledState44)).toEqual([ + expect(makeTopChirpsSelector(mockLaoId, 3).resultFunc(reactionFilledState5)).toEqual([ mockChirp2, mockChirp1, ]); From 4251c0cc5665cd9324850f20659d08905e91505d Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 31 Jan 2023 23:34:00 +0100 Subject: [PATCH 060/121] refactor validator by adding list instead of if else --- .../MessageDataContentValidator.scala | 26 +++- .../graph/validators/MessageValidator.scala | 16 +- .../graph/validators/RollCallValidator.scala | 142 +++++++++--------- 3 files changed, 110 insertions(+), 74 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala index 5c76d018ec..cc3b6c5334 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala @@ -1,10 +1,12 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef +import ch.epfl.pop.model.network.method.message.Message +import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} import ch.epfl.pop.model.network.method.message.data.election.ElectionQuestion import ch.epfl.pop.model.objects.{Hash, PublicKey, Timestamp, WitnessSignaturePair} import ch.epfl.pop.pubsub.AskPatternConstants -import ch.epfl.pop.pubsub.graph.{ErrorCodes, PipelineError} +import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor trait MessageDataContentValidator extends ContentValidator with AskPatternConstants { @@ -12,6 +14,16 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta def validationErrorNoMessage(rpcId: Option[Int]): PipelineError = PipelineError(ErrorCodes.INVALID_DATA.id, s"RPC-params does not contain any message", rpcId) + def checkParameters[T](rpcMessage: JsonRpcRequest): (GraphMessage, Message, Option[T]) = { + rpcMessage.getParamsMessage match { + case Some(message: Message) => + val message: Message = rpcMessage.getParamsMessage.get + val data: T = message.decodedData.get.asInstanceOf[T] + (Left(rpcMessage), message, Some(data)) + case _ => (Right(validationErrorNoMessage(rpcMessage.id)), null, None) + } + } + // Lower bound for a timestamp to not be stale final val TIMESTAMP_BASE_TIME: Timestamp = Timestamp(1577833200L) // 1st january 2020 @@ -24,6 +36,10 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta */ final def validateTimestampStaleness(timestamp: Timestamp): Boolean = TIMESTAMP_BASE_TIME < timestamp + def checkTimestampStaleness(rpcMessage: JsonRpcRequest, timestamp: Timestamp, error: PipelineError): GraphMessage = { + if (validateTimestampStaleness(timestamp)) Left(rpcMessage) else Right(error) + } + /** Check whether timestamp is not older than timestamp * * @param first @@ -35,6 +51,14 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta */ final def validateTimestampOrder(first: Timestamp, second: Timestamp): Boolean = first <= second + def checkTimestampOrder(rpcMessage: JsonRpcRequest, first: Timestamp, second: Timestamp, error: PipelineError): GraphMessage = { + if (validateTimestampOrder(first, second)) Left(rpcMessage) else Right(error) + } + + def checkId(rpcMessage: JsonRpcRequest, expectedId: Hash, id: Hash, error: PipelineError): GraphMessage = { + if (expectedId == id) Left(rpcMessage) else Right(error) + } + /** Check whether a list of public keys are valid or not * * @param witnesses diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index 6a74fe6544..d2f4acfe3c 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -1,12 +1,12 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef -import ch.epfl.pop.model.network.JsonRpcRequest +import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.ObjectType import ch.epfl.pop.model.objects.{Channel, Hash, PublicKey} import ch.epfl.pop.pubsub.AskPatternConstants -import ch.epfl.pop.pubsub.graph.GraphMessage +import ch.epfl.pop.pubsub.graph.{GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor import scala.concurrent.Await @@ -47,6 +47,10 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } + def checkAttendee(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + if (validateAttendee(sender, channel, dbActor)) Left(rpcMessage) else Right(error) + } + /** checks whether the sender of the JsonRpcRequest is the LAO owner * * @param sender @@ -64,6 +68,10 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } + def checkOwner(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + if (validateOwner(sender, channel, dbActor)) Left(rpcMessage) else Right(error) + } + /** checks whether the channel of the JsonRpcRequest is of the given type * * @param channelObjectType @@ -80,4 +88,8 @@ object MessageValidator extends ContentValidator with AskPatternConstants { case _ => false } } + + def checkChannelType(rpcMessage: JsonRpcRequest, channelObjectType: ObjectType.ObjectType, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + if (validateChannelType(channelObjectType, channel, dbActor)) Left(rpcMessage) else Right(error) + } } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index eb8561afb4..8d3f7b340a 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -1,10 +1,10 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef -import ch.epfl.pop.model.network.JsonRpcRequest +import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message -import ch.epfl.pop.model.network.method.message.data.ActionType.{CLOSE, CREATE, OPEN, REOPEN} -import ch.epfl.pop.model.network.method.message.data.ObjectType +import ch.epfl.pop.model.network.method.message.data.ActionType.{ActionType, CLOSE, CREATE, OPEN, REOPEN} +import ch.epfl.pop.model.network.method.message.data.{ActionType, ObjectType} import ch.epfl.pop.model.network.method.message.data.rollCall.{CloseRollCall, CreateRollCall, IOpenRollCall} import ch.epfl.pop.model.objects.{Channel, Hash, PublicKey, RollCallData} import ch.epfl.pop.pubsub.graph.validators.MessageValidator._ @@ -35,6 +35,27 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa override val EVENT_HASH_PREFIX: String = "R" + private def runList(list: List[GraphMessage]): GraphMessage = { + var result = list.head + var newlist = list + + while (result.isLeft && !newlist.tail.isEmpty) { + newlist = newlist.tail + result = newlist.head + } + result + } + + private def extractParameters[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { + val message: Message = rpcMessage.getParamsMessage.get + val data: T = message.decodedData.get.asInstanceOf[T] + val laoId: Hash = rpcMessage.extractLaoId + val sender: PublicKey = message.sender + val channel: Channel = rpcMessage.getParamsChannel + + (data, laoId, sender, channel) + } + /** @param laoId * LAO id of the channel * @return @@ -54,29 +75,27 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val data: CreateRollCall = message.decodedData.get.asInstanceOf[CreateRollCall] - - val laoId: Hash = rpcMessage.extractLaoId + val (data, laoId, sender, channel) = extractParameters[CreateRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings(EVENT_HASH_PREFIX, laoId.toString, data.creation.toString, data.name) - val sender: PublicKey = message.sender - val channel: Channel = rpcMessage.getParamsChannel - - if (!validateTimestampStaleness(data.creation)) { - Right(validationError(s"stale 'creation' timestamp (${data.creation})")) - } else if (!validateTimestampOrder(data.creation, data.proposed_start)) { - Right(validationError(s"'proposed_start' (${data.proposed_start}) timestamp is smaller than 'creation' (${data.creation})")) - } else if (!validateTimestampOrder(data.proposed_start, data.proposed_end)) { - Right(validationError(s"'proposed_end' (${data.proposed_end}) timestamp is smaller than 'proposed_start' (${data.proposed_start})")) - } else if (expectedRollCallId != data.id) { - Right(validationError(s"unexpected id")) - } else if (!validateOwner(sender, channel, dbActorRef)) { - Right(validationError(s"invalid sender $sender")) - } else if (!validateChannelType(ObjectType.LAO, channel, dbActorRef)) { - Right(validationError(s"trying to send a CreateRollCall message on a wrong type of channel $channel")) - } else { - Left(rpcMessage) - } + runList(List( + checkTimestampStaleness(rpcMessage, data.creation, validationError(s"stale 'creation' timestamp (${data.creation})")), + checkTimestampOrder( + rpcMessage, + data.creation, + data.proposed_start, + validationError(s"'proposed_start' (${data.proposed_start}) timestamp is smaller than 'creation' (${data.creation})") + ), + checkTimestampOrder( + rpcMessage, + data.proposed_start, + data.proposed_end, + validationError(s"'proposed_end' (${data.proposed_end}) timestamp is smaller than 'proposed_start' (${data.proposed_start})") + ), + checkId(rpcMessage, expectedRollCallId, data.id, validationError(s"unexpected id")), + checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), + checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a CreateRollCall message on a wrong type of channel $channel")) + )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } @@ -93,8 +112,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val data: IOpenRollCall = message.decodedData.get.asInstanceOf[IOpenRollCall] - val laoId: Hash = rpcMessage.extractLaoId + val (data, laoId, sender, channel) = extractParameters[IOpenRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings( EVENT_HASH_PREFIX, laoId.toString, @@ -102,32 +120,23 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.opened_at.toString ) - val sender: PublicKey = message.sender - val channel: Channel = rpcMessage.getParamsChannel - - if (!validateTimestampStaleness(data.opened_at)) { - Right(validationError(s"stale 'opened_at' timestamp (${data.opened_at})")) - } else if (expectedRollCallId != data.update_id) { - Right(validationError("unexpected id 'update_id'")) - } else if (!validateOpens(laoId, data.opens)) { - Right(validationError("unexpected id 'opens'")) - } else if (!validateOwner(sender, channel, dbActorRef)) { - Right(validationError(s"invalid sender $sender")) - } else if (!validateChannelType(ObjectType.LAO, channel, dbActorRef)) { - Right(validationError(s"trying to send a $validatorName message on a wrong type of channel $channel")) - } else { - Left(rpcMessage) - } + runList(List( + checkTimestampStaleness(rpcMessage, data.opened_at, validationError(s"stale 'opened_at' timestamp (${data.opened_at})")), + checkId(rpcMessage, expectedRollCallId, data.update_id, validationError("unexpected id 'update_id'")), + checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), + checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a $validatorName message on a wrong type of channel $channel")), + validateOpens(rpcMessage, laoId, data.opens, validationError("unexpected id 'opens'")) + )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } - private def validateOpens(laoId: Hash, opens: Hash): Boolean = { + private def validateOpens(rpcMessage: JsonRpcRequest, laoId: Hash, opens: Hash, error: PipelineError): GraphMessage = { val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { case Some(data) => - (data.state == CREATE || data.state == CLOSE) && data.updateId == opens - case _ => false + if ((data.state == CREATE || data.state == CLOSE) && data.updateId == opens) Left(rpcMessage) else Right(error) + case _ => Right(error) } } @@ -147,9 +156,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val data: CloseRollCall = message.decodedData.get.asInstanceOf[CloseRollCall] - - val laoId: Hash = rpcMessage.extractLaoId + val (data, laoId, sender, channel) = extractParameters[CloseRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings( EVENT_HASH_PREFIX, laoId.toString, @@ -157,35 +164,28 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.closed_at.toString ) - val sender: PublicKey = message.sender - val channel: Channel = rpcMessage.getParamsChannel - - if (!validateTimestampStaleness(data.closed_at)) { - Right(validationError(s"stale 'closed_at' timestamp (${data.closed_at})")) - } else if (data.attendees.size != data.attendees.toSet.size) { - Right(validationError("duplicate attendees keys")) - } else if (!validateAttendee(sender, channel, dbActorRef)) { - Right(validationError("unexpected attendees keys")) - } else if (expectedRollCallId != data.update_id) { - Right(validationError("unexpected id 'update_id'")) - } else if (!validateCloses(laoId, data.closes)) { - Right(validationError("unexpected id 'closes'")) - } else if (!validateOwner(sender, channel, dbActorRef)) { - Right(validationError(s"invalid sender $sender")) - } else if (!validateChannelType(ObjectType.LAO, channel, dbActorRef)) { - Right(validationError(s"trying to send a CloseRollCall message on a wrong type of channel $channel")) - } else { - Left(rpcMessage) - } + runList(List( + checkTimestampStaleness(rpcMessage, data.closed_at, validationError(s"stale 'closed_at' timestamp (${data.closed_at})")), + checkAttendeeSize(rpcMessage, data.attendees.size, data.attendees.toSet.size, validationError("duplicate attendees keys")), + checkAttendee(rpcMessage, sender, channel, dbActorRef, validationError("unexpected attendees keys")), + checkId(rpcMessage, expectedRollCallId, data.update_id, validationError("unexpected id 'update_id'")), + validateCloses(rpcMessage, laoId, data.closes, validationError("unexpected id 'closes'")), + checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), + checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a CloseRollCall message on a wrong type of channel $channel")) + )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } - private def validateCloses(laoId: Hash, closes: Hash): Boolean = { + private def validateCloses(rpcMessage: JsonRpcRequest, laoId: Hash, closes: Hash, error: PipelineError): GraphMessage = { val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { - case Some(data) => (data.state == OPEN || data.state == REOPEN) && data.updateId == closes - case _ => false + case Some(data) => if ((data.state == OPEN || data.state == REOPEN) && data.updateId == closes) Left(rpcMessage) else Right(error) + case _ => Right(error) } } + + private def checkAttendeeSize(rpcMessage: JsonRpcRequest, size: Int, expectedSize: Int, error: PipelineError): GraphMessage = { + if (size == expectedSize) Left(rpcMessage) else Right(error) + } } From a9c81c3589fd776fb77018745c5b0625cd915816 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Tue, 31 Jan 2023 23:36:14 +0100 Subject: [PATCH 061/121] Fix failing tests --- .../components/__tests__/ChirpCard.test.tsx | 37 +++++++++++++------ .../__snapshots__/ChirpCard.test.tsx.snap | 36 ++---------------- .../network/__mocks__/SocialMessageApi.ts | 1 + 3 files changed, 31 insertions(+), 43 deletions(-) diff --git a/fe1-web/src/features/social/components/__tests__/ChirpCard.test.tsx b/fe1-web/src/features/social/components/__tests__/ChirpCard.test.tsx index a651461977..552b40739b 100644 --- a/fe1-web/src/features/social/components/__tests__/ChirpCard.test.tsx +++ b/fe1-web/src/features/social/components/__tests__/ChirpCard.test.tsx @@ -7,10 +7,14 @@ import FeatureContext from 'core/contexts/FeatureContext'; import { useActionSheet } from 'core/hooks/ActionSheet'; import { Hash, PublicKey, Timestamp } from 'core/objects'; import { OpenedLaoStore } from 'features/lao/store'; +import { mockReaction1 } from 'features/social/__tests__/utils'; import { SocialMediaContext } from 'features/social/context'; import { SocialReactContext, SOCIAL_FEATURE_IDENTIFIER } from '../../interface'; -import { requestAddReaction as mockRequestAddReaction } from '../../network/SocialMessageApi'; +import { + requestAddReaction as mockRequestAddReaction, + requestDeleteReaction as mockRequestDeleteReaction, +} from '../../network/SocialMessageApi'; import { Chirp } from '../../objects'; import ChirpCard from '../ChirpCard'; @@ -52,14 +56,25 @@ const chirp1 = new Chirp({ // endregion jest.mock('features/social/network/SocialMessageApi'); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(() => ({ - 'πŸ‘': 1, - 'πŸ‘Ž': 0, - '❀️': 0, - })), -})); +jest.mock('react-redux', () => { + // use this to return the same values on the first and second call in each render + let x = 1; + + return { + ...jest.requireActual('react-redux'), + useSelector: jest.fn(() => { + x = (x + 1) % 2; + + if (x === 0) { + return { 'πŸ‘': 1, 'πŸ‘Ž': 0, '❀️': 0 }; + } + + return { + 'πŸ‘': mockReaction1, + }; + }), + }; +}); jest.mock('core/components/ProfileIcon', () => () => 'ProfileIcon'); @@ -131,11 +146,11 @@ describe('ChirpCard', () => { expect(obj.toJSON()).toMatchSnapshot(); }); - it('adds thumbs up correctly', () => { + it('removes thumbs up correctly', () => { const { getByTestId } = renderChirp(chirp, true); const thumbsUpButton = getByTestId('thumbs-up'); fireEvent.press(thumbsUpButton); - expect(mockRequestAddReaction).toHaveBeenCalledWith('πŸ‘', ID, mockLaoId); + expect(mockRequestDeleteReaction).toHaveBeenCalledWith(mockReaction1.id, mockLaoId); }); it('adds thumbs down correctly', () => { diff --git a/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap b/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap index e4e3719303..84cc528771 100644 --- a/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap +++ b/fe1-web/src/features/social/components/__tests__/__snapshots__/ChirpCard.test.tsx.snap @@ -1195,18 +1195,11 @@ exports[`ChirpCard for deletion renders correctly for non-sender 1`] = ` "borderWidth": 1, "padding": 8, }, - Object { - "backgroundColor": "#8E8E8E", - "borderColor": "#8E8E8E", - }, - Object { - "backgroundColor": "transparent", - }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} + {"name":"thumbs-up-sharp","size":16,"color":"#fff"} @@ -2070,18 +2063,11 @@ exports[`ChirpCard for deletion renders correctly for sender 1`] = ` "borderWidth": 1, "padding": 8, }, - Object { - "backgroundColor": "#8E8E8E", - "borderColor": "#8E8E8E", - }, - Object { - "backgroundColor": "transparent", - }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} + {"name":"thumbs-up-sharp","size":16,"color":"#fff"} @@ -3016,18 +3002,11 @@ exports[`ChirpCard for reaction renders correctly with reaction 1`] = ` "borderWidth": 1, "padding": 8, }, - Object { - "backgroundColor": "#8E8E8E", - "borderColor": "#8E8E8E", - }, - Object { - "backgroundColor": "transparent", - }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} + {"name":"thumbs-up-sharp","size":16,"color":"#fff"} @@ -3962,18 +3941,11 @@ exports[`ChirpCard for reaction renders correctly without reaction 1`] = ` "borderWidth": 1, "padding": 8, }, - Object { - "backgroundColor": "#8E8E8E", - "borderColor": "#8E8E8E", - }, - Object { - "backgroundColor": "transparent", - }, ] } > - {"name":"thumbs-up-sharp","size":16,"color":"#8E8E8E"} + {"name":"thumbs-up-sharp","size":16,"color":"#fff"} diff --git a/fe1-web/src/features/social/network/__mocks__/SocialMessageApi.ts b/fe1-web/src/features/social/network/__mocks__/SocialMessageApi.ts index b51a88e558..febcd30ef2 100644 --- a/fe1-web/src/features/social/network/__mocks__/SocialMessageApi.ts +++ b/fe1-web/src/features/social/network/__mocks__/SocialMessageApi.ts @@ -3,3 +3,4 @@ */ export const requestDeleteChirp = jest.fn(() => Promise.resolve()); export const requestAddReaction = jest.fn(() => Promise.resolve()); +export const requestDeleteReaction = jest.fn(() => Promise.resolve()); From 914f4805231201f5e0acdd974b5563fe12a45091 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 31 Jan 2023 23:50:33 +0100 Subject: [PATCH 062/121] remove unused code --- .../validators/MessageDataContentValidator.scala | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala index cc3b6c5334..cbb60a7c56 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala @@ -1,9 +1,7 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef -import ch.epfl.pop.model.network.method.message.Message -import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} -import ch.epfl.pop.model.network.method.message.data.election.ElectionQuestion +import ch.epfl.pop.model.network.{JsonRpcRequest} import ch.epfl.pop.model.objects.{Hash, PublicKey, Timestamp, WitnessSignaturePair} import ch.epfl.pop.pubsub.AskPatternConstants import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} @@ -14,16 +12,6 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta def validationErrorNoMessage(rpcId: Option[Int]): PipelineError = PipelineError(ErrorCodes.INVALID_DATA.id, s"RPC-params does not contain any message", rpcId) - def checkParameters[T](rpcMessage: JsonRpcRequest): (GraphMessage, Message, Option[T]) = { - rpcMessage.getParamsMessage match { - case Some(message: Message) => - val message: Message = rpcMessage.getParamsMessage.get - val data: T = message.decodedData.get.asInstanceOf[T] - (Left(rpcMessage), message, Some(data)) - case _ => (Right(validationErrorNoMessage(rpcMessage.id)), null, None) - } - } - // Lower bound for a timestamp to not be stale final val TIMESTAMP_BASE_TIME: Timestamp = Timestamp(1577833200L) // 1st january 2020 From 2063765ecb193d2e3c2b79809443731036cae3d7 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 1 Feb 2023 00:18:11 +0100 Subject: [PATCH 063/121] reduce line length --- .../pubsub/graph/handlers/SocialMediaHandler.scala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 9ac373edc4..e2e4f15437 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -2,7 +2,7 @@ package ch.epfl.pop.pubsub.graph.handlers import akka.pattern.AskableActorRef import ch.epfl.pop.json.MessageDataProtocol._ -import ch.epfl.pop.model.network.JsonRpcRequest +import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.socialMedia._ import ch.epfl.pop.model.objects._ @@ -40,6 +40,13 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { private def generateSocialChannel(lao_id: Hash): Channel = Channel(Channel.ROOT_CHANNEL_PREFIX + lao_id + Channel.SOCIAL_MEDIA_CHIRPS_PREFIX) + // the following function encodes the data and broadcast it + private def broadcastData[T](rpcMessage: JsonRpcRequest, data: T, channel: Channel, broadcastChannel: Channel): GraphMessage = { + val encodedData: Base64Data = Base64Data.encode(data.toString) + val broadcast: Future[GraphMessage] = dbBroadcast(rpcMessage, channel, encodedData, broadcastChannel) + Await.result(broadcast, duration) + } + def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { @@ -52,7 +59,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast(rpcMessage, channelChirp, Base64Data.encode(notifyAddChirp.toJson.toString), broadcastChannel), duration) + broadcastData[NotifyAddChirp](rpcMessage, notifyAddChirp, channelChirp, broadcastChannel) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } @@ -70,7 +77,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast(rpcMessage, channelChirp, Base64Data.encode(notifyDeleteChirp.toJson.toString), broadcastChannel), duration) + broadcastData[NotifyDeleteChirp](rpcMessage, notifyDeleteChirp, channelChirp, broadcastChannel) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } From c17d4e212c506492e69eabc4516f70484cbf6d7a Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 1 Feb 2023 00:30:48 +0100 Subject: [PATCH 064/121] split some lines --- .../ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala | 6 +++++- .../ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala index b7e319bcd7..55af8d1765 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/CoinHandler.scala @@ -21,7 +21,11 @@ case object CoinHandler extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => Left(rpcMessage) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handlePostTransaction failed : ${ex.message}", rpcMessage.getId)) - case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handlePostTransaction failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) + case reply => Right(PipelineError( + ErrorCodes.SERVER_ERROR.id, + s"handlePostTransaction failed : unexpected DbActor reply '$reply'", + rpcMessage.getId + )) } } } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala index 394fba0cbd..c81028ab4a 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MeetingHandler.scala @@ -21,7 +21,11 @@ case object MeetingHandler extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => Left(rpcMessage) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleCreateMeeting failed : ${ex.message}", rpcMessage.getId)) - case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handleCreateMeeting failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) + case reply => Right(PipelineError( + ErrorCodes.SERVER_ERROR.id, + s"handleCreateMeeting failed : unexpected DbActor reply '$reply'", + rpcMessage.getId + )) } } From b055f1940003c22e88c38e882b41bb53d95347af Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Wed, 1 Feb 2023 20:34:56 +0100 Subject: [PATCH 065/121] Update SocialReducer.ts --- fe1-web/src/features/social/reducer/SocialReducer.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fe1-web/src/features/social/reducer/SocialReducer.ts b/fe1-web/src/features/social/reducer/SocialReducer.ts index 67980db492..13261d876f 100644 --- a/fe1-web/src/features/social/reducer/SocialReducer.ts +++ b/fe1-web/src/features/social/reducer/SocialReducer.ts @@ -358,7 +358,7 @@ export const makeReactionCountsSelector = (laoId: Hash, chirpId: Hash) => ); // count them by codepoint - const reactionCodePointCounts = reactions.reduce>( + return reactions.reduce>( (counts, reaction) => { if (counts[reaction.codepoint]) { counts[reaction.codepoint] += 1; @@ -370,8 +370,6 @@ export const makeReactionCountsSelector = (laoId: Hash, chirpId: Hash) => }, { 'πŸ‘': 0, 'πŸ‘Ž': 0, '❀️': 0 }, ); - - return reactionCodePointCounts; }); export const makeReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey) => @@ -392,7 +390,7 @@ export const makeReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey ); // add reaction mapping for each code point if the user matches - const reactionByCodepoints = reactions.reduce>((obj, reaction) => { + return reactions.reduce>((obj, reaction) => { if (reaction.sender !== serializedPublicKey) { // skip reactions by other suers return obj; @@ -402,8 +400,6 @@ export const makeReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey return obj; }, {}); - - return reactionByCodepoints; }); export const makeTopChirpsSelector = (laoId: Hash, max: number) => From 78e828457858d7aabc0b35174af5f744040fe475 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Wed, 1 Feb 2023 21:00:37 +0100 Subject: [PATCH 066/121] Display time until nearest upcoming event --- .../features/events/components/EventLists.tsx | 20 ++++++++- .../__snapshots__/EventLists.test.tsx.snap | 42 +++++++++++++++++++ fe1-web/src/resources/strings.ts | 2 +- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/features/events/components/EventLists.tsx b/fe1-web/src/features/events/components/EventLists.tsx index f9c94f640f..08004d2402 100644 --- a/fe1-web/src/features/events/components/EventLists.tsx +++ b/fe1-web/src/features/events/components/EventLists.tsx @@ -4,6 +4,7 @@ import { ListItem } from '@rneui/themed'; import React, { useEffect, useMemo, useState } from 'react'; import { View } from 'react-native'; import { useSelector } from 'react-redux'; +import ReactTimeago from 'react-timeago'; import { AppParamList } from 'core/navigation/typing/AppParamList'; import { LaoEventsParamList } from 'core/navigation/typing/LaoEventsParamList'; @@ -53,9 +54,22 @@ const EventLists = () => { return () => clearInterval(interval); }, [events]); + // find upcoming event that is nearest / closest in the future + const closestUpcomingEvent = useMemo( + () => + upcomingEvents.reduce((closestEvent, event) => { + if (closestEvent === null || event.start < closestEvent.start) { + return event; + } + + return closestEvent; + }, null), + [upcomingEvents], + ); + return ( - {upcomingEvents.length > 0 && ( + {upcomingEvents.length > 0 && closestUpcomingEvent && ( { {STRINGS.events_upcoming_events} + + {STRINGS.events_closest_upcoming_event}{' '} + + diff --git a/fe1-web/src/features/events/components/__tests__/__snapshots__/EventLists.test.tsx.snap b/fe1-web/src/features/events/components/__tests__/__snapshots__/EventLists.test.tsx.snap index 6e68a657ac..0cdb6c9218 100644 --- a/fe1-web/src/features/events/components/__tests__/__snapshots__/EventLists.test.tsx.snap +++ b/fe1-web/src/features/events/components/__tests__/__snapshots__/EventLists.test.tsx.snap @@ -470,6 +470,27 @@ exports[`EventLists renders correctly for attendees 1`] = ` > Upcoming Events + + Next Event starts + + + Upcoming Events + + Next Event starts + + + Date: Wed, 1 Feb 2023 21:08:21 +0100 Subject: [PATCH 067/121] Fix error message --- fe1-web/src/features/home/screens/ConnectScan.tsx | 4 +++- fe1-web/src/resources/strings.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/fe1-web/src/features/home/screens/ConnectScan.tsx b/fe1-web/src/features/home/screens/ConnectScan.tsx index 71fd217c0b..6c38a2ad4d 100644 --- a/fe1-web/src/features/home/screens/ConnectScan.tsx +++ b/fe1-web/src/features/home/screens/ConnectScan.tsx @@ -173,6 +173,8 @@ const ConnectScan = () => { params: { screen: STRINGS.navigation_lao_events_home }, }); } catch (error) { + console.error(error); + // close already established connections getNetworkManager().disconnectFromAll(); @@ -180,7 +182,7 @@ const ConnectScan = () => { isProcessingScan.current = false; setIsConnecting(false); - toast.show(STRINGS.connect_scanning_fail, { + toast.show(STRINGS.connect_connecting_fail, { type: 'danger', placement: 'bottom', duration: FOUR_SECONDS, diff --git a/fe1-web/src/resources/strings.ts b/fe1-web/src/resources/strings.ts index 3f99c34de9..433fd8a897 100644 --- a/fe1-web/src/resources/strings.ts +++ b/fe1-web/src/resources/strings.ts @@ -165,6 +165,7 @@ namespace STRINGS { export const connect_description = 'The easiest way to connect to a local organization is to scan its QR code'; export const connect_scanning_fail = 'Invalid QRCode data'; + export const connect_connecting_fail = 'Failed connecting and subscribing to LAO'; // Connecting Connect Strings export const connect_connect = 'Connect'; From c7427dd1fc5122047256fa33f54cb7a53219b9d9 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Wed, 1 Feb 2023 21:11:27 +0100 Subject: [PATCH 068/121] Prevent text overflow on lao item --- fe1-web/src/features/lao/components/LaoItem.tsx | 4 +++- .../components/__tests__/__snapshots__/LaoItem.test.tsx.snap | 3 +++ .../components/__tests__/__snapshots__/LaoList.test.tsx.snap | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fe1-web/src/features/lao/components/LaoItem.tsx b/fe1-web/src/features/lao/components/LaoItem.tsx index f0fec09a7e..eb008ee78a 100644 --- a/fe1-web/src/features/lao/components/LaoItem.tsx +++ b/fe1-web/src/features/lao/components/LaoItem.tsx @@ -83,7 +83,9 @@ const LaoItem = ({ lao, isFirstItem, isLastItem }: IPropTypes) => { onPress={reconnectToLao} bottomDivider> - {lao.name} + + {lao.name} + {STRINGS.user_role}: {role} diff --git a/fe1-web/src/features/lao/components/__tests__/__snapshots__/LaoItem.test.tsx.snap b/fe1-web/src/features/lao/components/__tests__/__snapshots__/LaoItem.test.tsx.snap index 1aec21007f..ee5012c0bb 100644 --- a/fe1-web/src/features/lao/components/__tests__/__snapshots__/LaoItem.test.tsx.snap +++ b/fe1-web/src/features/lao/components/__tests__/__snapshots__/LaoItem.test.tsx.snap @@ -444,6 +444,7 @@ exports[`LaoItem renders correctly as attendee 1`] = ` > Date: Wed, 1 Feb 2023 21:18:59 +0100 Subject: [PATCH 069/121] Move build info --- fe1-web/src/core/components/BuildInfo.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/core/components/BuildInfo.tsx b/fe1-web/src/core/components/BuildInfo.tsx index 2c0a8c02fc..500bf37da8 100644 --- a/fe1-web/src/core/components/BuildInfo.tsx +++ b/fe1-web/src/core/components/BuildInfo.tsx @@ -7,12 +7,13 @@ import { Spacing, Typography } from 'core/styles'; const styles = StyleSheet.create({ container: { position: 'absolute', - bottom: Spacing.x05, + top: Spacing.x3, left: Spacing.x05, + right: Spacing.x05, zIndex: 100, display: 'flex', flexDirection: 'row', - alignItems: 'center', + justifyContent: 'center', } as ViewStyle, }); From a3cd5eb059d64de722bb8119c52638e6fc5da784 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Wed, 1 Feb 2023 21:20:15 +0100 Subject: [PATCH 070/121] Update BuildInfo.test.tsx.snap --- .../__tests__/__snapshots__/BuildInfo.test.tsx.snap | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/core/components/__tests__/__snapshots__/BuildInfo.test.tsx.snap b/fe1-web/src/core/components/__tests__/__snapshots__/BuildInfo.test.tsx.snap index 8e38b127aa..a37bf80a7e 100644 --- a/fe1-web/src/core/components/__tests__/__snapshots__/BuildInfo.test.tsx.snap +++ b/fe1-web/src/core/components/__tests__/__snapshots__/BuildInfo.test.tsx.snap @@ -4,12 +4,13 @@ exports[`BuildInfo renders correctly 1`] = ` Date: Wed, 1 Feb 2023 21:29:18 +0100 Subject: [PATCH 071/121] Display election status on election detail screens --- .../evoting/components/ElectionHeader.tsx | 12 ++- .../ViewSingleElection.test.tsx.snap | 90 +++++++++++++++++++ fe1-web/src/resources/strings.ts | 5 ++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/fe1-web/src/features/evoting/components/ElectionHeader.tsx b/fe1-web/src/features/evoting/components/ElectionHeader.tsx index b1500b2a53..442057055b 100644 --- a/fe1-web/src/features/evoting/components/ElectionHeader.tsx +++ b/fe1-web/src/features/evoting/components/ElectionHeader.tsx @@ -4,8 +4,16 @@ import { Text } from 'react-native'; import DateRange from 'core/components/DateRange'; import { Typography } from 'core/styles'; +import STRINGS from 'resources/strings'; -import { Election } from '../objects'; +import { Election, ElectionStatus } from '../objects'; + +const LABEL_BY_ELECTION_STATUS: Record = { + [ElectionStatus.NOT_STARTED]: STRINGS.election_status_not_started, + [ElectionStatus.OPENED]: STRINGS.election_status_opened, + [ElectionStatus.TERMINATED]: STRINGS.election_status_terminated, + [ElectionStatus.RESULT]: STRINGS.election_status_results, +}; const ElectionHeader = ({ election }: IPropTypes) => { return ( @@ -15,6 +23,8 @@ const ElectionHeader = ({ election }: IPropTypes) => { + {'\n'} + {LABEL_BY_ELECTION_STATUS[election.electionStatus]} ); }; diff --git a/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/ViewSingleElection.test.tsx.snap b/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/ViewSingleElection.test.tsx.snap index 08c49aa7ff..f79e74f8fd 100644 --- a/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/ViewSingleElection.test.tsx.snap +++ b/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/ViewSingleElection.test.tsx.snap @@ -408,6 +408,21 @@ exports[`ViewSingleElection renders correctly non organizers election with resul + + + + Ended, Results available + + + + + Not Started + + + + + Ended + + + + + Ended, Results available + + + + + Not Started + + + + + Ended + Date: Thu, 2 Feb 2023 01:08:18 +0100 Subject: [PATCH 072/121] recursive runList --- .../pubsub/graph/validators/RollCallValidator.scala | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index 8d3f7b340a..ba41829b38 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -36,14 +36,10 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa override val EVENT_HASH_PREFIX: String = "R" private def runList(list: List[GraphMessage]): GraphMessage = { - var result = list.head - var newlist = list - - while (result.isLeft && !newlist.tail.isEmpty) { - newlist = newlist.tail - result = newlist.head - } - result + if (list.head.isLeft && !list.tail.isEmpty) + runList(list.tail) + else + list.head } private def extractParameters[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { From e9e20c1637257fb31217909af45106afc530801e Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Thu, 2 Feb 2023 01:16:37 +0100 Subject: [PATCH 073/121] change name extractData + move to MessageValidator file --- .../graph/validators/MessageValidator.scala | 17 ++++++++++++++ .../graph/validators/RollCallValidator.scala | 23 +++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index d2f4acfe3c..62dc07d2b3 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -92,4 +92,21 @@ object MessageValidator extends ContentValidator with AskPatternConstants { def checkChannelType(rpcMessage: JsonRpcRequest, channelObjectType: ObjectType.ObjectType, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { if (validateChannelType(channelObjectType, channel, dbActor)) Left(rpcMessage) else Right(error) } + + def extractData[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { + val message: Message = rpcMessage.getParamsMessage.get + val data: T = message.decodedData.get.asInstanceOf[T] + val laoId: Hash = rpcMessage.extractLaoId + val sender: PublicKey = message.sender + val channel: Channel = rpcMessage.getParamsChannel + + (data, laoId, sender, channel) + } + + def runList(list: List[GraphMessage]): GraphMessage = { + if (list.head.isLeft && !list.tail.isEmpty) + runList(list.tail) + else + list.head + } } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index ba41829b38..d5c2820c3c 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -35,23 +35,6 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa override val EVENT_HASH_PREFIX: String = "R" - private def runList(list: List[GraphMessage]): GraphMessage = { - if (list.head.isLeft && !list.tail.isEmpty) - runList(list.tail) - else - list.head - } - - private def extractParameters[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { - val message: Message = rpcMessage.getParamsMessage.get - val data: T = message.decodedData.get.asInstanceOf[T] - val laoId: Hash = rpcMessage.extractLaoId - val sender: PublicKey = message.sender - val channel: Channel = rpcMessage.getParamsChannel - - (data, laoId, sender, channel) - } - /** @param laoId * LAO id of the channel * @return @@ -71,7 +54,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val (data, laoId, sender, channel) = extractParameters[CreateRollCall](rpcMessage) + val (data, laoId, sender, channel) = extractData[CreateRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings(EVENT_HASH_PREFIX, laoId.toString, data.creation.toString, data.name) runList(List( @@ -108,7 +91,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val (data, laoId, sender, channel) = extractParameters[IOpenRollCall](rpcMessage) + val (data, laoId, sender, channel) = extractData[IOpenRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings( EVENT_HASH_PREFIX, laoId.toString, @@ -152,7 +135,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => - val (data, laoId, sender, channel) = extractParameters[CloseRollCall](rpcMessage) + val (data, laoId, sender, channel) = extractData[CloseRollCall](rpcMessage) val expectedRollCallId: Hash = Hash.fromStrings( EVENT_HASH_PREFIX, laoId.toString, From 333b75ad6518a452dd847adfbb568f6ce2964449 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Thu, 2 Feb 2023 13:39:39 +0100 Subject: [PATCH 074/121] clean election test --- be1-go/channel/election/mod_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/be1-go/channel/election/mod_test.go b/be1-go/channel/election/mod_test.go index b061890a37..517ddcbb6b 100644 --- a/be1-go/channel/election/mod_test.go +++ b/be1-go/channel/election/mod_test.go @@ -476,14 +476,11 @@ func Test_Sending_Election_Key(t *testing.T) { electionKeyMsg := catchupAnswer[1] - data := messagedata.ElectionKey{}.NewEmpty() + var dataKey messagedata.ElectionKey - err := electionKeyMsg.UnmarshalData(data) + err := electionKeyMsg.UnmarshalData(&dataKey) require.NoError(t, err, electionKeyMsg) - dataKey, ok := data.(*messagedata.ElectionKey) - require.True(t, ok) - key, err := base64.URLEncoding.DecodeString(dataKey.Key) require.NoError(t, err) From ca5f011060939c52b17e9d4470894897f90b322c Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Thu, 2 Feb 2023 13:40:51 +0100 Subject: [PATCH 075/121] plit non cached test in another command --- be1-go/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/be1-go/Makefile b/be1-go/Makefile index 9a11849a34..f1155e3bf9 100644 --- a/be1-go/Makefile +++ b/be1-go/Makefile @@ -9,6 +9,9 @@ lint: staticcheck ./... test: protocol + go test -v -race ./... + +test-no-cache: protocol go test -v -race ./... -count=1 test-cov: protocol From 876405704685ab2d81fedb53917b893f56fce6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johann=20Pl=C3=BCss?= <45600803+Warhedgehog@users.noreply.github.com> Date: Sat, 4 Feb 2023 22:36:03 +0100 Subject: [PATCH 076/121] Slight improvement Co-authored-by: Gabriel Fleischer --- .../java/com/github/dedis/popstellar/utility/Constants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java index d6265c72b7..b518b9af01 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.java @@ -58,5 +58,5 @@ public class Constants { public static final int QR_SIDE = 800; /** Number of milliseconds in a day */ - public static final long MS_IN_A_DAY = 86_400_000L; + public static final long MS_IN_A_DAY = 1000 * 60 * 60 * 24L; } From fba19cd453d37ae911acbfe304d959d4cc99e3a4 Mon Sep 17 00:00:00 2001 From: Johann Pluss Date: Sat, 4 Feb 2023 22:38:01 +0100 Subject: [PATCH 077/121] Remove outdated comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../popstellar/ui/detail/event/eventlist/EventsAdapter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java index 2dd20f507e..dea1d885ea 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/event/eventlist/EventsAdapter.java @@ -54,7 +54,6 @@ private void subscribeToEventSet(Observable> observable) { this.viewModel.addDisposable( observable .map(eventList -> eventList.stream().sorted().collect(Collectors.toList())) - // No need to check for error as the events errors already handles them .subscribe(this::updateEventSet, err -> Log.e(tag, "ERROR", err))); } From 3f3b420707b292339c0b8c8098d7511da2cfaa4f Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 5 Feb 2023 20:04:49 +0100 Subject: [PATCH 078/121] Fix bug in transaction validation --- .../objects/transaction/Transaction.ts | 18 +++++++- .../__tests__/Transaction.test.tsx | 46 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts index f329957a66..2f0c1faab5 100644 --- a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts +++ b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts @@ -81,6 +81,7 @@ export class Transaction { this.transactionId = this.hashTransaction(); } else { if (obj.transactionId.valueOf() !== this.hashTransaction().valueOf()) { + console.error(this.hashTransaction().valueOf(), obj.transactionId.valueOf()); throw new Error( "The computed transaction hash does not correspond to the provided one when creating 'Transaction'", ); @@ -286,8 +287,21 @@ export class Transaction { return false; } } else { - const originTransactionOutput = - transactionStates[input.txOutHash.valueOf()].outputs[input.txOutIndex]; + const txOut = input.txOutHash.valueOf(); + + if (!(txOut in transactionStates)) { + console.warn(`Transaction refers to unkown input transaction '${txOut}'`); + return false; + } + + if (input.txOutIndex >= transactionStates[input.txOutHash.valueOf()].outputs.length) { + console.warn( + `Transaction refers to unkown output index '${input.txOutIndex}' of transaction '${txOut}'`, + ); + return false; + } + + const originTransactionOutput = transactionStates[txOut].outputs[input.txOutIndex]; // The public key hash of the used transaction output must correspond // to the public key the transaction is using in this input diff --git a/fe1-web/src/features/digital-cash/objects/transaction/__tests__/Transaction.test.tsx b/fe1-web/src/features/digital-cash/objects/transaction/__tests__/Transaction.test.tsx index 192afa3bff..251873530f 100644 --- a/fe1-web/src/features/digital-cash/objects/transaction/__tests__/Transaction.test.tsx +++ b/fe1-web/src/features/digital-cash/objects/transaction/__tests__/Transaction.test.tsx @@ -1,7 +1,7 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockKeyPair, mockPublicKey2 } from '__tests__/utils'; +import { mockKeyPair, mockPublicKey2 as serializedMockPublicKey2 } from '__tests__/utils'; import { Hash, PopToken, PublicKey, Timestamp } from 'core/objects'; import { COINBASE_HASH, SCRIPT_TYPE } from 'resources/const'; @@ -18,6 +18,7 @@ import { TransactionOutput } from '../TransactionOutput'; // region Mock value definitions const mockPopToken = PopToken.fromState(mockKeyPair.toState()); +const mockPublicKey2 = new PublicKey(serializedMockPublicKey2); const validCoinbaseState: TransactionState = { version: 1, @@ -293,7 +294,7 @@ describe('Transaction', () => { mockTransactionValue, ); const isValid = coinbaseTransaction.checkTransactionValidity( - new PublicKey(mockPublicKey2), + mockPublicKey2, mockTransactionRecordByHash, ); expect(isValid).toBeFalse(); @@ -308,10 +309,49 @@ describe('Transaction', () => { [validCoinbaseState], ); const isValid = transaction.checkTransactionValidity( - new PublicKey(mockPublicKey2), + mockPublicKey2, mockTransactionRecordByHash, ); expect(isValid).toBeTrue(); }); + + it('should not accept transactions referring to unkown input', () => { + const transaction = Transaction.create( + mockPopToken, + mockKeyPair.publicKey, + mockTransactionValue, + mockTransactionValue, + [validCoinbaseState], + ); + const isValid = transaction.checkTransactionValidity(mockPublicKey2, {}); + expect(isValid).toBeFalse(); + }); + + it('should not accept transactions referring to unkown input index', () => { + const transaction = Transaction.create( + mockPopToken, + mockKeyPair.publicKey, + mockTransactionValue, + mockTransactionValue, + [validCoinbaseState], + ); + const json = transaction.toJSON(); + + const invalidTransactionId = Hash.fromState('gobrqgypCyrZoD3Qz294MOjcR9eO9NUo07VgGPSgTYQ='); + + const invalidTransaction = Transaction.fromJSON( + { + ...json, + inputs: [{ ...json.inputs[0], tx_out_index: 100 }], + }, + invalidTransactionId, + ); + + const isValid = invalidTransaction.checkTransactionValidity( + mockPublicKey2, + mockTransactionRecordByHash, + ); + expect(isValid).toBeFalse(); + }); }); }); From 48ddf30fda07b1e4c0563418ba074027ee7030de Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 5 Feb 2023 20:37:24 +0100 Subject: [PATCH 079/121] Add more try/catch clauses --- .../network/DigitalCashHandler.ts | 9 +- .../evoting/network/ElectionHandler.ts | 69 ++++++++------ .../src/features/lao/network/LaoHandler.ts | 43 ++++++--- .../meeting/network/MeetingHandler.ts | 54 ++++++----- .../rollCall/network/RollCallHandler.ts | 90 ++++++++++++------- .../features/social/network/ChirpHandler.ts | 40 ++++++--- .../social/network/ReactionHandler.ts | 21 +++-- .../witness/network/WitnessHandler.ts | 17 ++-- 8 files changed, 227 insertions(+), 116 deletions(-) diff --git a/fe1-web/src/features/digital-cash/network/DigitalCashHandler.ts b/fe1-web/src/features/digital-cash/network/DigitalCashHandler.ts index 5b19da3fde..c9ddb7f5e0 100644 --- a/fe1-web/src/features/digital-cash/network/DigitalCashHandler.ts +++ b/fe1-web/src/features/digital-cash/network/DigitalCashHandler.ts @@ -41,7 +41,14 @@ export const handleTransactionPost = return false; } - const transaction = Transaction.fromJSON(tx.transaction, tx.transaction_id); + let transaction: Transaction; + + try { + transaction = Transaction.fromJSON(tx.transaction, tx.transaction_id); + } catch (e: any) { + console.warn(`handleTransactionPost failed: ${e?.toString()}`); + return false; + } // Check the transaction signatures over the inputs and outputs, // and check that the transaction inputs used are consistent with our current state diff --git a/fe1-web/src/features/evoting/network/ElectionHandler.ts b/fe1-web/src/features/evoting/network/ElectionHandler.ts index e18a956ab3..60ff461cf5 100644 --- a/fe1-web/src/features/evoting/network/ElectionHandler.ts +++ b/fe1-web/src/features/evoting/network/ElectionHandler.ts @@ -107,18 +107,25 @@ export const handleElectionSetupMessage = return false; } - const election = new Election({ - lao: elecMsg.lao, - id: elecMsg.id, - name: elecMsg.name, - version: elecMsg.version as ElectionVersion, - createdAt: elecMsg.created_at, - start: elecMsg.start_time, - end: elecMsg.end_time, - questions: elecMsg.questions, - electionStatus: ElectionStatus.NOT_STARTED, - registeredVotes: [], - }); + let election: Election; + + try { + election = new Election({ + lao: elecMsg.lao, + id: elecMsg.id, + name: elecMsg.name, + version: elecMsg.version as ElectionVersion, + createdAt: elecMsg.created_at, + start: elecMsg.start_time, + end: elecMsg.end_time, + questions: elecMsg.questions, + electionStatus: ElectionStatus.NOT_STARTED, + registeredVotes: [], + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } // Subscribing to the election channel corresponding to that election const electionChannel = channelFromIds(election.lao, election.id); @@ -213,12 +220,19 @@ export const handleCastVoteMessage = return false; } - const currentVote = new RegisteredVote({ - createdAt: castVoteMsg.created_at, - sender: msg.sender, - votes: castVoteMsg.votes, - messageId: msg.message_id, - }); + let currentVote: RegisteredVote; + + try { + currentVote = new RegisteredVote({ + createdAt: castVoteMsg.created_at, + sender: msg.sender, + votes: castVoteMsg.votes, + messageId: msg.message_id, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } if (election.registeredVotes.some((votes) => votes.sender.equals(currentVote.sender))) { // Update the vote if the person has already voted before @@ -339,13 +353,18 @@ export const handleElectionResultMessage = return false; } - election.questionResult = electionResultMessage.questions.map( - (q) => - new QuestionResult({ - id: new Hash(q.id), - result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), - }), - ); + try { + election.questionResult = electionResultMessage.questions.map( + (q) => + new QuestionResult({ + id: new Hash(q.id), + result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), + }), + ); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } election.electionStatus = ElectionStatus.RESULT; updateElection(election); diff --git a/fe1-web/src/features/lao/network/LaoHandler.ts b/fe1-web/src/features/lao/network/LaoHandler.ts index d8ac6a0285..91c04278cf 100644 --- a/fe1-web/src/features/lao/network/LaoHandler.ts +++ b/fe1-web/src/features/lao/network/LaoHandler.ts @@ -14,15 +14,23 @@ export function handleLaoCreateMessage(msg: ProcessableMessage): boolean { } const createLaoMsg = msg.messageData as CreateLao; - const lao = new Lao({ - id: createLaoMsg.id, - name: createLaoMsg.name, - creation: createLaoMsg.creation, - last_modified: createLaoMsg.creation, - organizer: createLaoMsg.organizer, - witnesses: createLaoMsg.witnesses, - server_addresses: [msg.receivedFrom], - }); + + let lao: Lao; + + try { + lao = new Lao({ + id: createLaoMsg.id, + name: createLaoMsg.name, + creation: createLaoMsg.creation, + last_modified: createLaoMsg.creation, + organizer: createLaoMsg.organizer, + witnesses: createLaoMsg.witnesses, + server_addresses: [msg.receivedFrom], + }); + } catch (e: any) { + console.warn(`handleLaoCreateMessage failed processing lao#create message: ${e?.toString()}`); + return false; + } dispatch(setCurrentLao(lao, true)); return true; @@ -65,11 +73,18 @@ export function handleLaoStateMessage(msg: ProcessableMessage): boolean { } const updateLaoData = updateMessage.messageData as UpdateLao; - const lao = new Lao({ - ...oldLao, - name: updateLaoData.name, - witnesses: updateLaoData.witnesses, - }); + let lao: Lao; + + try { + lao = new Lao({ + ...oldLao, + name: updateLaoData.name, + witnesses: updateLaoData.witnesses, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } dispatch(updateLao(lao.toState())); return true; */ diff --git a/fe1-web/src/features/meeting/network/MeetingHandler.ts b/fe1-web/src/features/meeting/network/MeetingHandler.ts index 852dd4a302..81fc2ca7fb 100644 --- a/fe1-web/src/features/meeting/network/MeetingHandler.ts +++ b/fe1-web/src/features/meeting/network/MeetingHandler.ts @@ -38,15 +38,22 @@ export const handleMeetingCreateMessage = const meetingMessage = msg.messageData as CreateMeeting; - const meeting = new Meeting({ - id: meetingMessage.id, - name: meetingMessage.name, - location: meetingMessage.location, - creation: meetingMessage.creation, - start: meetingMessage.start, - end: meetingMessage.end ? meetingMessage.end : undefined, - extra: meetingMessage.extra ? { ...meetingMessage.extra } : {}, - }); + let meeting: Meeting; + + try { + meeting = new Meeting({ + id: meetingMessage.id, + name: meetingMessage.name, + location: meetingMessage.location, + creation: meetingMessage.creation, + start: meetingMessage.start, + end: meetingMessage.end ? meetingMessage.end : undefined, + extra: meetingMessage.extra ? { ...meetingMessage.extra } : {}, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } addMeeting(msg.laoId, meeting); return true; @@ -100,17 +107,24 @@ export const handleMeetingStateMessage = return false; } - const meeting = new Meeting({ - ...oldMeeting, - lastModified: meetingMessage.last_modified, - location: meetingMessage.location, - start: meetingMessage.start, - end: meetingMessage.end, - extra: { - ...oldMeeting.extra, - ...meetingMessage.extra, - }, - }); + let meeting: Meeting; + + try { + meeting = new Meeting({ + ...oldMeeting, + lastModified: meetingMessage.last_modified, + location: meetingMessage.location, + start: meetingMessage.start, + end: meetingMessage.end, + extra: { + ...oldMeeting.extra, + ...meetingMessage.extra, + }, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } updateMeeting(meeting); return true; diff --git a/fe1-web/src/features/rollCall/network/RollCallHandler.ts b/fe1-web/src/features/rollCall/network/RollCallHandler.ts index 1b071413eb..7e86d53de3 100644 --- a/fe1-web/src/features/rollCall/network/RollCallHandler.ts +++ b/fe1-web/src/features/rollCall/network/RollCallHandler.ts @@ -35,16 +35,23 @@ export const handleRollCallCreateMessage = const rcMsgData = msg.messageData as CreateRollCall; - const rollCall = new RollCall({ - id: rcMsgData.id, - name: rcMsgData.name, - location: rcMsgData.location, - description: rcMsgData.description, - creation: rcMsgData.creation, - proposedStart: rcMsgData.proposed_start, - proposedEnd: rcMsgData.proposed_end, - status: RollCallStatus.CREATED, - }); + let rollCall; + + try { + rollCall = new RollCall({ + id: rcMsgData.id, + name: rcMsgData.name, + location: rcMsgData.location, + description: rcMsgData.description, + creation: rcMsgData.creation, + proposedStart: rcMsgData.proposed_start, + proposedEnd: rcMsgData.proposed_end, + status: RollCallStatus.CREATED, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } addRollCall(msg.laoId, rollCall); return true; @@ -87,12 +94,19 @@ export const handleRollCallOpenMessage = return false; } - const rollCall = new RollCall({ - ...oldRC, - idAlias: rcMsgData.update_id, - openedAt: rcMsgData.opened_at, - status: RollCallStatus.OPENED, - }); + let rollCall: RollCall; + + try { + rollCall = new RollCall({ + ...oldRC, + idAlias: rcMsgData.update_id, + openedAt: rcMsgData.opened_at, + status: RollCallStatus.OPENED, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } updateRollCall(rollCall); return true; @@ -141,13 +155,20 @@ export const handleRollCallCloseMessage = return false; } - const rollCall = new RollCall({ - ...oldRC, - idAlias: rcMsgData.update_id, - closedAt: rcMsgData.closed_at, - status: RollCallStatus.CLOSED, - attendees: rcMsgData.attendees, - }); + let rollCall: RollCall; + + try { + rollCall = new RollCall({ + ...oldRC, + idAlias: rcMsgData.update_id, + closedAt: rcMsgData.closed_at, + status: RollCallStatus.CLOSED, + attendees: rcMsgData.attendees, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } updateRollCall(rollCall); @@ -243,14 +264,21 @@ export const handleRollCallReopenMessage = return false; } - const rollCall = new RollCall({ - ...oldRC, - idAlias: rcMsgData.update_id, - openedAt: rcMsgData.opened_at, - status: RollCallStatus.REOPENED, - // unset end as the roll call is open once again - closedAt: undefined, - }); + let rollCall: RollCall; + + try { + rollCall = new RollCall({ + ...oldRC, + idAlias: rcMsgData.update_id, + openedAt: rcMsgData.opened_at, + status: RollCallStatus.REOPENED, + // unset end as the roll call is open once again + closedAt: undefined, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } updateRollCall(rollCall); return true; diff --git a/fe1-web/src/features/social/network/ChirpHandler.ts b/fe1-web/src/features/social/network/ChirpHandler.ts index 229e413531..e3eee2b824 100644 --- a/fe1-web/src/features/social/network/ChirpHandler.ts +++ b/fe1-web/src/features/social/network/ChirpHandler.ts @@ -32,13 +32,20 @@ export const handleAddChirpMessage = const { sender } = msg; const chirpMessage = msg.messageData as AddChirp; - const chirp = new Chirp({ - id: messageId, - sender: sender, - text: chirpMessage.text, - time: chirpMessage.timestamp, - parentId: chirpMessage.parent_id, - }); + let chirp: Chirp; + + try { + chirp = new Chirp({ + id: messageId, + sender: sender, + text: chirpMessage.text, + time: chirpMessage.timestamp, + parentId: chirpMessage.parent_id, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } dispatch(addChirp(laoId, chirp)); return true; @@ -68,12 +75,19 @@ export const handleDeleteChirpMessage = const { sender } = msg; const chirpMessage = msg.messageData as DeleteChirp; - const chirp = new Chirp({ - id: chirpMessage.chirp_id, - sender: sender, - time: chirpMessage.timestamp, - text: '', - }); + let chirp: Chirp; + + try { + chirp = new Chirp({ + id: chirpMessage.chirp_id, + sender: sender, + time: chirpMessage.timestamp, + text: '', + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } dispatch(deleteChirp(laoId, chirp)); return true; diff --git a/fe1-web/src/features/social/network/ReactionHandler.ts b/fe1-web/src/features/social/network/ReactionHandler.ts index 13ce0ed162..3b929f2199 100644 --- a/fe1-web/src/features/social/network/ReactionHandler.ts +++ b/fe1-web/src/features/social/network/ReactionHandler.ts @@ -35,13 +35,20 @@ export const handleAddReactionMessage = const { sender } = msg; const reactionMessage = msg.messageData as AddReaction; - const reaction = new Reaction({ - id: messageId, - sender: sender, - codepoint: reactionMessage.reaction_codepoint, - chirpId: reactionMessage.chirp_id, - time: reactionMessage.timestamp, - }); + let reaction: Reaction; + + try { + reaction = new Reaction({ + id: messageId, + sender: sender, + codepoint: reactionMessage.reaction_codepoint, + chirpId: reactionMessage.chirp_id, + time: reactionMessage.timestamp, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } dispatch(addReaction(laoId, reaction)); return true; diff --git a/fe1-web/src/features/witness/network/WitnessHandler.ts b/fe1-web/src/features/witness/network/WitnessHandler.ts index 304010ea07..fe37c93198 100644 --- a/fe1-web/src/features/witness/network/WitnessHandler.ts +++ b/fe1-web/src/features/witness/network/WitnessHandler.ts @@ -35,12 +35,18 @@ export const handleWitnessMessage = } const wsMsgData = msg.messageData as WitnessMessage; - const msgId = wsMsgData.message_id; - const ws = new WitnessSignature({ - witness: msg.sender, - signature: wsMsgData.signature, - }); + + let ws: WitnessSignature; + try { + ws = new WitnessSignature({ + witness: msg.sender, + signature: wsMsgData.signature, + }); + } catch (e: any) { + console.warn(makeErr(e?.toString())); + return false; + } if (!ws.verify(msgId)) { console.warn( @@ -48,6 +54,7 @@ export const handleWitnessMessage = `signature by ${ws.witness} doesn't match message ${msgId}`, msg, ); + return false; } From 951117068180ece55fba78762718231bb449dded Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 5 Feb 2023 23:01:14 +0100 Subject: [PATCH 080/121] Add tests --- .../evoting/network/ElectionHandler.ts | 19 +- .../network/__tests__/ElectionHandler.test.ts | 48 ++++- .../evoting/objects/QuestionResult.ts | 19 +- .../evoting/objects/RegisteredVote.ts | 26 +++ .../objects/__tests__/QuestionResult.test.ts | 47 +++++ .../objects/__tests__/RegisteredVote.test.ts | 81 ++++++++ .../network/__tests__/RollCallHandler.test.ts | 65 ++++++ .../src/features/rollCall/objects/RollCall.ts | 18 ++ .../objects/__tests__/RollCall.test.ts | 128 +++++++++++- .../__tests__/ViewSingleRollCall.test.tsx | 3 +- .../network/__tests__/ChirpHandler.test.ts | 188 ++++++++++++++++++ .../network/__tests__/ReactionHandler.test.ts | 121 +++++++++++ 12 files changed, 745 insertions(+), 18 deletions(-) create mode 100644 fe1-web/src/features/evoting/objects/__tests__/QuestionResult.test.ts create mode 100644 fe1-web/src/features/evoting/objects/__tests__/RegisteredVote.test.ts create mode 100644 fe1-web/src/features/social/network/__tests__/ChirpHandler.test.ts create mode 100644 fe1-web/src/features/social/network/__tests__/ReactionHandler.test.ts diff --git a/fe1-web/src/features/evoting/network/ElectionHandler.ts b/fe1-web/src/features/evoting/network/ElectionHandler.ts index 60ff461cf5..8becda46fe 100644 --- a/fe1-web/src/features/evoting/network/ElectionHandler.ts +++ b/fe1-web/src/features/evoting/network/ElectionHandler.ts @@ -353,18 +353,13 @@ export const handleElectionResultMessage = return false; } - try { - election.questionResult = electionResultMessage.questions.map( - (q) => - new QuestionResult({ - id: new Hash(q.id), - result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), - }), - ); - } catch (e: any) { - console.warn(makeErr(e?.toString())); - return false; - } + election.questionResult = electionResultMessage.questions.map( + (q) => + new QuestionResult({ + id: new Hash(q.id), + result: q.result.map((r) => ({ ballotOption: r.ballot_option, count: r.count })), + }), + ); election.electionStatus = ElectionStatus.RESULT; updateElection(election); diff --git a/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts b/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts index 2c90688483..e6b30919ad 100644 --- a/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts +++ b/fe1-web/src/features/evoting/network/__tests__/ElectionHandler.test.ts @@ -34,7 +34,7 @@ import { } from 'features/evoting/__tests__/utils'; import { addElectionKey } from 'features/evoting/reducer/ElectionKeyReducer'; -import { Election, ElectionStatus, QuestionResult, RegisteredVote } from '../../objects'; +import { Election, ElectionStatus, QuestionResult, RegisteredVote, Vote } from '../../objects'; import { handleCastVoteMessage, handleElectionEndMessage, @@ -251,6 +251,28 @@ describe('ElectionHandler', () => { ).toBeFalse(); }); + it('should return false if there is an issue with the message data', () => { + const addEvent = jest.fn(); + + expect( + handleElectionSetupMessage(addEvent)({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.SETUP, + lao: mockLaoId, + id: undefined as unknown as Hash, + name: mockElectionNotStarted.name, + version: mockElectionNotStarted.version, + created_at: mockElectionNotStarted.createdAt, + start_time: mockElectionNotStarted.start, + end_time: mockElectionNotStarted.end, + questions: mockElectionNotStarted.questions, + } as SetupElection, + }), + ).toBeFalse(); + }); + it('should create the election', () => { const addEvent = jest.fn(); @@ -455,6 +477,30 @@ describe('ElectionHandler', () => { ).toBeFalse(); }); + it('it should return false if something is off with the message data', () => { + const mockElection = Election.fromState({ + ...mockElectionOpened.toState(), + registeredVotes: [], + }); + + expect( + handleCastVoteMessage( + mockGetEventById.mockImplementationOnce(() => Election.fromState(mockElection.toState())), + mockUpdateEvent, + )({ + ...mockMessageData, + messageData: { + object: ObjectType.ELECTION, + action: ActionType.CAST_VOTE, + election: mockElectionId, + created_at: TIMESTAMP, + lao: mockLaoId, + votes: undefined as unknown as Vote[], + } as CastVote, + }), + ).toBeFalse(); + }); + it('it should update election.registeredVotes', () => { const mockElection = Election.fromState({ ...mockElectionOpened.toState(), diff --git a/fe1-web/src/features/evoting/objects/QuestionResult.ts b/fe1-web/src/features/evoting/objects/QuestionResult.ts index 5eb72ba293..0fc15a847e 100644 --- a/fe1-web/src/features/evoting/objects/QuestionResult.ts +++ b/fe1-web/src/features/evoting/objects/QuestionResult.ts @@ -1,4 +1,4 @@ -import { Hash, HashState } from 'core/objects'; +import { Hash, HashState, ProtocolError } from 'core/objects'; import { OmitMethods } from 'core/types'; export interface MajorityResult { @@ -17,7 +17,24 @@ export class QuestionResult { result: MajorityResult[]; constructor(questionResult: OmitMethods) { + if (!questionResult) { + throw new Error( + 'Error encountered while creating a QuestionResult object: undefined/null parameters', + ); + } + + if (!questionResult.id) { + throw new ProtocolError( + "Undefined 'messageId' parameter encountered during 'QuestionResult'", + ); + } this.id = questionResult.id; + + if (!questionResult.result) { + throw new ProtocolError( + "Undefined 'messageId' parameter encountered during 'QuestionResult'", + ); + } this.result = questionResult.result; } diff --git a/fe1-web/src/features/evoting/objects/RegisteredVote.ts b/fe1-web/src/features/evoting/objects/RegisteredVote.ts index fddb517016..54a896b4b4 100644 --- a/fe1-web/src/features/evoting/objects/RegisteredVote.ts +++ b/fe1-web/src/features/evoting/objects/RegisteredVote.ts @@ -1,6 +1,7 @@ import { Hash, HashState, + ProtocolError, PublicKey, PublicKeyState, Timestamp, @@ -28,9 +29,34 @@ export class RegisteredVote { public readonly createdAt: Timestamp; constructor(registeredVote: OmitMethods) { + if (!registeredVote) { + throw new Error( + 'Error encountered while creating a RegisteredVote object: undefined/null parameters', + ); + } + + if (!registeredVote.messageId) { + throw new ProtocolError( + "Undefined 'messageId' parameter encountered during 'RegisteredVote'", + ); + } this.messageId = registeredVote.messageId; + + if (!registeredVote.sender) { + throw new ProtocolError("Undefined 'sender' parameter encountered during 'RegisteredVote'"); + } this.sender = registeredVote.sender; + + if (!registeredVote.votes) { + throw new ProtocolError("Undefined 'votes' parameter encountered during 'RegisteredVote'"); + } this.votes = registeredVote.votes; + + if (!registeredVote.createdAt) { + throw new ProtocolError( + "Undefined 'createdAt' parameter encountered during 'RegisteredVote'", + ); + } this.createdAt = registeredVote.createdAt; } diff --git a/fe1-web/src/features/evoting/objects/__tests__/QuestionResult.test.ts b/fe1-web/src/features/evoting/objects/__tests__/QuestionResult.test.ts new file mode 100644 index 0000000000..ae35e7c759 --- /dev/null +++ b/fe1-web/src/features/evoting/objects/__tests__/QuestionResult.test.ts @@ -0,0 +1,47 @@ +import 'jest-extended'; +import '__tests__/utils/matchers'; + +import { Hash } from 'core/objects'; +import { OmitMethods } from 'core/types'; + +import { MajorityResult, QuestionResult } from '../index'; + +const mockQuestionId = new Hash('questionId'); + +const questionResult = new QuestionResult({ + id: mockQuestionId, + result: [{ ballotOption: 'option1', count: 10 }], +}); + +describe('QuestionResult', () => { + it('does a state round trip correctly', () => { + const e = QuestionResult.fromState(questionResult.toState()); + expect(e).toEqual(questionResult); + }); + + describe('constructor', () => { + it('throws an error when object is undefined', () => { + const partial = undefined as unknown as OmitMethods; + const createWrongQuestionResult = () => new QuestionResult(partial); + expect(createWrongQuestionResult).toThrow(Error); + }); + + it("throws an error when 'id' is undefined", () => { + const createWrongRegisteredVote = () => + new QuestionResult({ + id: undefined as unknown as Hash, + result: [{ ballotOption: 'option1', count: 10 }], + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + + it("throws an error when 'result' is undefined", () => { + const createWrongRegisteredVote = () => + new QuestionResult({ + id: mockQuestionId, + result: undefined as unknown as MajorityResult[], + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + }); +}); diff --git a/fe1-web/src/features/evoting/objects/__tests__/RegisteredVote.test.ts b/fe1-web/src/features/evoting/objects/__tests__/RegisteredVote.test.ts new file mode 100644 index 0000000000..666dd3457e --- /dev/null +++ b/fe1-web/src/features/evoting/objects/__tests__/RegisteredVote.test.ts @@ -0,0 +1,81 @@ +import 'jest-extended'; +import '__tests__/utils/matchers'; + +import { Hash, PublicKey, Timestamp } from 'core/objects'; +import { OmitMethods } from 'core/types'; + +import { RegisteredVote, Vote } from '../index'; + +const vote1 = new Vote({ + id: new Hash('v1'), + question: new Hash('q1'), + vote: 0, +}); + +const mockMessageId = new Hash('messageId1'); + +const registeredVotes = new RegisteredVote({ + createdAt: new Timestamp(1520255700), + sender: new PublicKey('Sender1'), + votes: [vote1], + messageId: mockMessageId, +}); + +describe('RegisteredVote', () => { + it('does a state round trip correctly', () => { + const e = RegisteredVote.fromState(registeredVotes.toState()); + expect(e).toEqual(registeredVotes); + }); + + describe('constructor', () => { + it('throws an error when object is undefined', () => { + const partial = undefined as unknown as OmitMethods; + const createWrongRegisteredVote = () => new RegisteredVote(partial); + expect(createWrongRegisteredVote).toThrow(Error); + }); + + it("throws an error when 'messageId' is undefined", () => { + const createWrongRegisteredVote = () => + new RegisteredVote({ + createdAt: new Timestamp(1520255700), + sender: new PublicKey('Sender1'), + votes: [vote1], + messageId: undefined as unknown as Hash, + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + + it("throws an error when 'sender' is undefined", () => { + const createWrongRegisteredVote = () => + new RegisteredVote({ + createdAt: new Timestamp(1520255700), + sender: undefined as unknown as PublicKey, + votes: [vote1], + messageId: mockMessageId, + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + + it("throws an error when 'votes' is undefined", () => { + const createWrongRegisteredVote = () => + new RegisteredVote({ + createdAt: new Timestamp(1520255700), + sender: new PublicKey('Sender1'), + votes: undefined as unknown as Vote[], + messageId: mockMessageId, + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + + it("throws an error when 'createdAt' is undefined", () => { + const createWrongRegisteredVote = () => + new RegisteredVote({ + createdAt: undefined as unknown as Timestamp, + sender: new PublicKey('Sender1'), + votes: [vote1], + messageId: mockMessageId, + }); + expect(createWrongRegisteredVote).toThrow(Error); + }); + }); +}); diff --git a/fe1-web/src/features/rollCall/network/__tests__/RollCallHandler.test.ts b/fe1-web/src/features/rollCall/network/__tests__/RollCallHandler.test.ts index 8341efa369..795d2fda9b 100644 --- a/fe1-web/src/features/rollCall/network/__tests__/RollCallHandler.test.ts +++ b/fe1-web/src/features/rollCall/network/__tests__/RollCallHandler.test.ts @@ -10,6 +10,7 @@ import { import { Base64UrlData, Hash, Signature, Timestamp } from 'core/objects'; import { RollCall, RollCallStatus } from 'features/rollCall/objects'; +import { CloseRollCall, CreateRollCall, OpenRollCall, ReopenRollCall } from '../messages'; import { handleRollCallCloseMessage, handleRollCallCreateMessage, @@ -128,6 +129,19 @@ describe('RollCallHandler', () => { ).toBeFalse(); }); + it('should return false if something is off with the message data', () => { + const message = createMockMsg(ActionType.CREATE, rollCallStateCreated); + expect( + handleRollCallCreateMessage(jest.fn())({ + ...message, + messageData: { + ...message.messageData, + id: undefined as unknown as Hash, + } as CreateRollCall, + }), + ).toBeFalse(); + }); + it('should create a correct RollCall object from msgData', async () => { const usedMockMsg = createMockMsg(ActionType.CREATE, rollCallStateCreated); @@ -184,6 +198,22 @@ describe('RollCallHandler', () => { ).toBeFalse(); }); + it('should return false if there is an issue with the message data', () => { + const message = createMockMsg(ActionType.OPEN, rollCallStateOpened); + expect( + handleRollCallOpenMessage( + () => RollCall.fromState(mockRollCallCreated.toState()), + jest.fn(), + )({ + ...message, + messageData: { + ...message.messageData, + update_id: undefined as unknown as Hash, + } as OpenRollCall, + }), + ).toBeFalse(); + }); + it('should create a correct RollCall object from msgData', async () => { const usedMockMsg = createMockMsg(ActionType.OPEN, rollCallStateOpened); @@ -249,6 +279,25 @@ describe('RollCallHandler', () => { ).toBeFalse(); }); + it('should return false in case of issues with the message data', () => { + const message = createMockMsg(ActionType.CLOSE, rollCallStateOpened); + + expect( + handleRollCallCloseMessage( + jest.fn(() => RollCall.fromState(mockRollCallOpened.toState())), + jest.fn(), + () => Promise.resolve(mockPopToken), + jest.fn(), + )({ + ...message, + messageData: { + ...message.messageData, + closed_at: undefined as unknown as Timestamp, + } as CloseRollCall, + }), + ).toBeFalse(); + }); + it('should create a correct RollCall object from msgData in handleRollCallCloseMessage', async () => { const usedMockMsg = createMockMsg(ActionType.CLOSE, rollCallStateClosed); @@ -315,6 +364,22 @@ describe('RollCallHandler', () => { ).toBeFalse(); }); + it('should return false in case of issues with the message data', () => { + const message = createMockMsg(ActionType.REOPEN, rollCallStateOpened); + expect( + handleRollCallReopenMessage( + jest.fn(() => mockRollCallClosed), + jest.fn(), + )({ + ...message, + messageData: { + ...message.messageData, + opened_at: undefined as unknown as Timestamp, + } as ReopenRollCall, + }), + ).toBeFalse(); + }); + it('should create a correct RollCall object from msgData', async () => { const usedMockMsg = createMockMsg(ActionType.REOPEN, rollCallStateReopened); diff --git a/fe1-web/src/features/rollCall/objects/RollCall.ts b/fe1-web/src/features/rollCall/objects/RollCall.ts index d684752247..34939a0dd7 100644 --- a/fe1-web/src/features/rollCall/objects/RollCall.ts +++ b/fe1-web/src/features/rollCall/objects/RollCall.ts @@ -103,6 +103,24 @@ export class RollCall { throw new Error("Undefined 'status' when creating 'RollCall'"); } + if (obj.status !== RollCallStatus.CREATED && !obj.idAlias) { + throw new Error( + `Error when creating 'RollCall': 'idAlias' can only be undefined in status 'CREATED'`, + ); + } + + if (obj.status !== RollCallStatus.CREATED && !obj.openedAt) { + throw new Error( + `Error when creating 'RollCall': 'openedAt' can only be undefined in status 'CREATED'`, + ); + } + + if (obj.status === RollCallStatus.CLOSED && !obj.closedAt) { + throw new Error( + `Error when creating 'RollCall': 'closedAt' cannot be undefined when in status 'CLOSED'`, + ); + } + this.id = obj.id; this.idAlias = obj.idAlias; this.name = obj.name; diff --git a/fe1-web/src/features/rollCall/objects/__tests__/RollCall.test.ts b/fe1-web/src/features/rollCall/objects/__tests__/RollCall.test.ts index bb47161058..cf9cdf1a2e 100644 --- a/fe1-web/src/features/rollCall/objects/__tests__/RollCall.test.ts +++ b/fe1-web/src/features/rollCall/objects/__tests__/RollCall.test.ts @@ -19,22 +19,25 @@ describe('RollCall object', () => { it('can do a state round trip correctly 1', () => { const rollCallState: any = { id: ID.valueOf(), + idAlias: ID.valueOf(), name: NAME, location: LOCATION, creation: TIMESTAMP_START.valueOf(), proposedStart: TIMESTAMP_START.valueOf(), proposedEnd: TIMESTAMP_END.valueOf(), + openedAt: TIMESTAMP_START, + closedAt: TIMESTAMP_END, status: RollCallStatus.CLOSED, attendees: ATTENDEES, }; const expected = { id: ID.valueOf(), + idAlias: ID.valueOf(), name: NAME, location: LOCATION, - closedAt: undefined, + closedAt: TIMESTAMP_END.valueOf(), description: undefined, - idAlias: undefined, - openedAt: undefined, + openedAt: TIMESTAMP_START.valueOf(), creation: TIMESTAMP_START.valueOf(), proposedStart: TIMESTAMP_START.valueOf(), proposedEnd: TIMESTAMP_END.valueOf(), @@ -80,11 +83,14 @@ describe('RollCall object', () => { it('containsToken function works when attendees is undefined', () => { const rollCall = new RollCall({ id: ID, + idAlias: ID, name: NAME, location: LOCATION, creation: TIMESTAMP_START, proposedStart: TIMESTAMP_START, proposedEnd: TIMESTAMP_END, + openedAt: TIMESTAMP_START, + closedAt: TIMESTAMP_END, status: RollCallStatus.CLOSED, }); expect(rollCall.containsToken(token)).toBeFalse(); @@ -93,11 +99,14 @@ describe('RollCall object', () => { it('containsToken function works when token is undefined', () => { const rollCall = new RollCall({ id: ID, + idAlias: ID, name: NAME, location: LOCATION, creation: TIMESTAMP_START, proposedStart: TIMESTAMP_START, proposedEnd: TIMESTAMP_END, + openedAt: TIMESTAMP_START, + closedAt: TIMESTAMP_END, status: RollCallStatus.CLOSED, attendees: ATTENDEES.map((s: string) => new PublicKey(s)), }); @@ -107,11 +116,14 @@ describe('RollCall object', () => { it('containsToken function works when attendees and token are defined', () => { const rollCall = new RollCall({ id: ID, + idAlias: ID, name: NAME, location: LOCATION, creation: TIMESTAMP_START, proposedStart: TIMESTAMP_START, proposedEnd: TIMESTAMP_END, + openedAt: TIMESTAMP_START, + closedAt: TIMESTAMP_END, status: RollCallStatus.CLOSED, attendees: ATTENDEES.map((s: string) => new PublicKey(s)), }); @@ -222,5 +234,115 @@ describe('RollCall object', () => { }); expect(createWrongRollCall).toThrow(Error); }); + + it("throws an error when 'idAlias' is undefined in status 'OPENED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.OPENED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'opened' is undefined in status 'OPENED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + idAlias: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.OPENED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'idAlias' is undefined in status 'REOPENED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.REOPENED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'openedAt' is undefined in status 'REOPENED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + idAlias: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.REOPENED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'idAlias' is undefined in status 'CLOSED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.CLOSED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'openedAt' is undefined in status 'CLOSED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + idAlias: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + status: RollCallStatus.CLOSED, + }); + expect(createWrongRollCall).toThrow(Error); + }); + + it("throws an error when 'closedAt' is undefined in status 'CLOSED'", () => { + const createWrongRollCall = () => + new RollCall({ + id: ID, + idAlias: ID, + start: TIMESTAMP_START, + name: NAME, + location: LOCATION, + creation: TIMESTAMP_START, + proposedStart: TIMESTAMP_START, + proposedEnd: TIMESTAMP_END, + openedAt: TIMESTAMP_START, + status: RollCallStatus.CLOSED, + }); + expect(createWrongRollCall).toThrow(Error); + }); }); }); diff --git a/fe1-web/src/features/rollCall/screens/__tests__/ViewSingleRollCall.test.tsx b/fe1-web/src/features/rollCall/screens/__tests__/ViewSingleRollCall.test.tsx index f61823de59..66d5b09816 100644 --- a/fe1-web/src/features/rollCall/screens/__tests__/ViewSingleRollCall.test.tsx +++ b/fe1-web/src/features/rollCall/screens/__tests__/ViewSingleRollCall.test.tsx @@ -31,12 +31,13 @@ const createStateWithStatus: any = (mockStatus: RollCallStatus) => { id: ID.valueOf(), eventType: RollCall.EVENT_TYPE, start: TIMESTAMP_START.valueOf(), - closedAt: TIMESTAMP_END.valueOf(), name: NAME, location: LOCATION, creation: TIMESTAMP_START.valueOf(), proposedStart: TIMESTAMP_START.valueOf(), proposedEnd: TIMESTAMP_END.valueOf(), + openedAt: TIMESTAMP_START.valueOf(), + closedAt: TIMESTAMP_END.valueOf(), status: mockStatus, attendees: ATTENDEES, idAlias: mockStatus === RollCallStatus.CREATED ? undefined : ID.valueOf(), diff --git a/fe1-web/src/features/social/network/__tests__/ChirpHandler.test.ts b/fe1-web/src/features/social/network/__tests__/ChirpHandler.test.ts new file mode 100644 index 0000000000..26a1346c9f --- /dev/null +++ b/fe1-web/src/features/social/network/__tests__/ChirpHandler.test.ts @@ -0,0 +1,188 @@ +import 'jest-extended'; +import '__tests__/utils/matchers'; + +import { mockAddress, mockKeyPair, mockLaoId } from '__tests__/utils'; +import { ActionType, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; +import { Base64UrlData, getGeneralChirpsChannel, Hash, Signature, Timestamp } from 'core/objects'; +import { dispatch } from 'core/redux'; +import { Chirp } from 'features/social/objects'; +import { addChirp, deleteChirp } from 'features/social/reducer'; + +import { handleAddChirpMessage, handleDeleteChirpMessage } from '../ChirpHandler'; +import { AddChirp, DeleteChirp } from '../messages/chirp'; + +const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 + +const mockMessageId = new Hash('some string'); +const mockSender = mockKeyPair.publicKey; +const mockAddChirp = new AddChirp({ text: 'text', timestamp: TIMESTAMP }); +const mockDeleteChirp = new DeleteChirp({ chirp_id: mockMessageId, timestamp: TIMESTAMP }); + +const mockMessageData = { + receivedAt: TIMESTAMP, + receivedFrom: mockAddress, + laoId: mockLaoId, + data: Base64UrlData.encode('some data'), + sender: mockSender, + signature: Base64UrlData.encode('some data') as Signature, + channel: getGeneralChirpsChannel(mockLaoId), + message_id: mockMessageId, + witness_signatures: [], +}; + +const getCurrentLaoId = () => mockLaoId; + +jest.mock('core/redux', () => { + const actualModule = jest.requireActual('core/redux'); + return { + ...actualModule, + dispatch: jest.fn((...args) => actualModule.dispatch(...args)), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('handleAddChirpMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleAddChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.REACTION, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleAddChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.DELETE, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is no current lao', () => { + expect( + handleAddChirpMessage(() => undefined)({ + ...mockMessageData, + messageData: mockAddChirp, + }), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleAddChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.ADD, + text: undefined as unknown as string, + timestamp: TIMESTAMP, + } as AddChirp, + }), + ).toBeFalse(); + }); + + it('should return true for valid messages', () => { + expect( + handleAddChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: mockAddChirp, + }), + ).toBeTrue(); + + expect(dispatch).toHaveBeenCalledWith( + addChirp( + mockLaoId, + new Chirp({ + id: mockMessageId, + sender: mockSender, + text: mockAddChirp.text, + time: mockAddChirp.timestamp, + parentId: mockAddChirp.parent_id, + }), + ), + ); + expect(dispatch).toHaveBeenCalledTimes(1); + }); +}); + +describe('handleDeleteChirpMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleDeleteChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.REACTION, + action: ActionType.DELETE, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleDeleteChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is no current lao', () => { + expect( + handleDeleteChirpMessage(() => undefined)({ + ...mockMessageData, + messageData: mockDeleteChirp, + }), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleDeleteChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.DELETE, + chirp_id: undefined as unknown as Hash, + timestamp: undefined as unknown as Timestamp, + } as DeleteChirp, + }), + ).toBeFalse(); + }); + + it('should return true for valid messages', () => { + expect( + handleDeleteChirpMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: mockDeleteChirp, + }), + ).toBeTrue(); + + expect(dispatch).toHaveBeenCalledWith( + deleteChirp( + mockLaoId, + new Chirp({ + id: mockMessageId, + sender: mockSender, + time: mockDeleteChirp.timestamp, + text: '', + }), + ), + ); + expect(dispatch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/fe1-web/src/features/social/network/__tests__/ReactionHandler.test.ts b/fe1-web/src/features/social/network/__tests__/ReactionHandler.test.ts new file mode 100644 index 0000000000..297f7594fb --- /dev/null +++ b/fe1-web/src/features/social/network/__tests__/ReactionHandler.test.ts @@ -0,0 +1,121 @@ +import 'jest-extended'; +import '__tests__/utils/matchers'; + +import { mockAddress, mockKeyPair, mockLaoId } from '__tests__/utils'; +import { ActionType, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; +import { Base64UrlData, getGeneralChirpsChannel, Hash, Signature, Timestamp } from 'core/objects'; +import { dispatch } from 'core/redux'; +import { Reaction } from 'features/social/objects'; +import { addReaction } from 'features/social/reducer'; + +import { AddReaction } from '../messages/reaction'; +import { handleAddReactionMessage } from '../ReactionHandler'; + +const TIMESTAMP = new Timestamp(1609455600); // 1st january 2021 + +const mockMessageId = new Hash('some string'); +const mockSender = mockKeyPair.publicKey; +const mockAddReaction = new AddReaction({ + chirp_id: mockMessageId, + reaction_codepoint: 'πŸ‘', + timestamp: TIMESTAMP, +}); + +const mockMessageData = { + receivedAt: TIMESTAMP, + receivedFrom: mockAddress, + laoId: mockLaoId, + data: Base64UrlData.encode('some data'), + sender: mockSender, + signature: Base64UrlData.encode('some data') as Signature, + channel: getGeneralChirpsChannel(mockLaoId), + message_id: mockMessageId, + witness_signatures: [], +}; + +const getCurrentLaoId = () => mockLaoId; + +jest.mock('core/redux', () => { + const actualModule = jest.requireActual('core/redux'); + return { + ...actualModule, + dispatch: jest.fn((...args) => actualModule.dispatch(...args)), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('handleAddReactionMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleAddReactionMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleAddReactionMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.REACTION, + action: ActionType.DELETE, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is no current lao', () => { + expect( + handleAddReactionMessage(() => undefined)({ + ...mockMessageData, + messageData: mockAddReaction, + }), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleAddReactionMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.CHIRP, + action: ActionType.ADD, + chirp_id: undefined as unknown as Hash, + reaction_codepoint: 'πŸ‘', + timestamp: TIMESTAMP, + } as AddReaction, + }), + ).toBeFalse(); + }); + + it('should return true for valid messages', () => { + expect( + handleAddReactionMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: mockAddReaction, + }), + ).toBeTrue(); + + expect(dispatch).toHaveBeenCalledWith( + addReaction( + mockLaoId, + new Reaction({ + id: mockMessageId, + sender: mockSender, + codepoint: mockAddReaction.reaction_codepoint, + chirpId: mockAddReaction.chirp_id, + time: mockAddReaction.timestamp, + }), + ), + ); + expect(dispatch).toHaveBeenCalledTimes(1); + }); +}); From 7d6a14231bbdcf8cef20cd0e3d8283bf6355407c Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 5 Feb 2023 23:02:23 +0100 Subject: [PATCH 081/121] Fix test --- .../src/features/wallet/screens/__tests__/WalletHome.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fe1-web/src/features/wallet/screens/__tests__/WalletHome.test.tsx b/fe1-web/src/features/wallet/screens/__tests__/WalletHome.test.tsx index b275405d33..35d05611ce 100644 --- a/fe1-web/src/features/wallet/screens/__tests__/WalletHome.test.tsx +++ b/fe1-web/src/features/wallet/screens/__tests__/WalletHome.test.tsx @@ -50,6 +50,8 @@ const mockRollCallState = { creation: mockRCTimestampStart.valueOf(), proposedStart: mockRCTimestampStart.valueOf(), proposedEnd: mockRCTimestampEnd.valueOf(), + openedAt: mockRCTimestampStart.valueOf(), + closedAt: mockRCTimestampEnd.valueOf(), status: RollCallStatus.CLOSED, attendees: mockRCAttendees, }; From 2a6b004fed239202db9f993770e8cdb14fa381fd Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Sun, 5 Feb 2023 23:18:56 +0100 Subject: [PATCH 082/121] Refactor checkTransactionValidity --- .../objects/transaction/Transaction.ts | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts index 2f0c1faab5..2d33e4fd92 100644 --- a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts +++ b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts @@ -280,47 +280,51 @@ export class Transaction { let totalInputAmount = 0; const inputsAreValid = this.inputs.every((input) => { + // The public key of this input must have signed the concatenated data + if (!input.script.signature.verify(input.script.publicKey, encodedData)) { + console.warn('The signature for this input is not valid'); + return false; + } + if (isCoinbase) { // If the transaction is a coinbase transaction, the signer must be the organizer if (input.script.publicKey.valueOf() !== organizerPublicKey.valueOf()) { console.warn('The coinbase transaction input signer is not the organizer'); return false; } - } else { - const txOut = input.txOutHash.valueOf(); + return true; + } - if (!(txOut in transactionStates)) { - console.warn(`Transaction refers to unkown input transaction '${txOut}'`); - return false; - } + // if it is not a coinbase transaction we need to verify the inputs + const txOut = input.txOutHash.valueOf(); - if (input.txOutIndex >= transactionStates[input.txOutHash.valueOf()].outputs.length) { - console.warn( - `Transaction refers to unkown output index '${input.txOutIndex}' of transaction '${txOut}'`, - ); - return false; - } + if (!(txOut in transactionStates)) { + console.warn(`Transaction refers to unkown input transaction '${txOut}'`); + return false; + } - const originTransactionOutput = transactionStates[txOut].outputs[input.txOutIndex]; - - // The public key hash of the used transaction output must correspond - // to the public key the transaction is using in this input - if ( - Hash.fromPublicKey(input.script.publicKey).valueOf() !== - originTransactionOutput.script.publicKeyHash.valueOf() - ) { - console.warn( - "The transaction output public key hash does not correspond to the spender's public key hash", - ); - return false; - } - totalInputAmount += originTransactionOutput.value; + if (input.txOutIndex >= transactionStates[input.txOutHash.valueOf()].outputs.length) { + console.warn( + `Transaction refers to unkown output index '${input.txOutIndex}' of transaction '${txOut}'`, + ); + return false; } - // The public key of this input must have signed the concatenated data - if (!input.script.signature.verify(input.script.publicKey, encodedData)) { - console.warn('The signature for this input is not valid'); + + const originTransactionOutput = transactionStates[txOut].outputs[input.txOutIndex]; + + // The public key hash of the used transaction output must correspond + // to the public key the transaction is using in this input + if ( + Hash.fromPublicKey(input.script.publicKey).valueOf() !== + originTransactionOutput.script.publicKeyHash.valueOf() + ) { + console.warn( + "The transaction output public key hash does not correspond to the spender's public key hash", + ); return false; } + totalInputAmount += originTransactionOutput.value; + return true; }); From 96c9a96a129dae3eb55e4b88e27d60b63e8d18fc Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Mon, 6 Feb 2023 08:52:09 +0100 Subject: [PATCH 083/121] Refactor checkTransactionValidity --- .../objects/transaction/Transaction.ts | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts index 2d33e4fd92..8f10a36abd 100644 --- a/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts +++ b/fe1-web/src/features/digital-cash/objects/transaction/Transaction.ts @@ -279,54 +279,60 @@ export class Transaction { let totalInputAmount = 0; - const inputsAreValid = this.inputs.every((input) => { - // The public key of this input must have signed the concatenated data + // The public key of this input must have signed the concatenated data + const inputSignaturesAreValid = this.inputs.every((input) => { if (!input.script.signature.verify(input.script.publicKey, encodedData)) { console.warn('The signature for this input is not valid'); return false; } - if (isCoinbase) { - // If the transaction is a coinbase transaction, the signer must be the organizer - if (input.script.publicKey.valueOf() !== organizerPublicKey.valueOf()) { - console.warn('The coinbase transaction input signer is not the organizer'); - return false; - } - return true; - } + return true; + }); - // if it is not a coinbase transaction we need to verify the inputs - const txOut = input.txOutHash.valueOf(); + const inputsAreValid = + inputSignaturesAreValid && + this.inputs.every((input) => { + if (isCoinbase) { + // If the transaction is a coinbase transaction, the signer must be the organizer + if (input.script.publicKey.valueOf() !== organizerPublicKey.valueOf()) { + console.warn('The coinbase transaction input signer is not the organizer'); + return false; + } + return true; + } - if (!(txOut in transactionStates)) { - console.warn(`Transaction refers to unkown input transaction '${txOut}'`); - return false; - } + // if it is not a coinbase transaction we need to verify the inputs + const txOut = input.txOutHash.valueOf(); - if (input.txOutIndex >= transactionStates[input.txOutHash.valueOf()].outputs.length) { - console.warn( - `Transaction refers to unkown output index '${input.txOutIndex}' of transaction '${txOut}'`, - ); - return false; - } + if (!(txOut in transactionStates)) { + console.warn(`Transaction refers to unkown input transaction '${txOut}'`); + return false; + } - const originTransactionOutput = transactionStates[txOut].outputs[input.txOutIndex]; + if (input.txOutIndex >= transactionStates[txOut].outputs.length) { + console.warn( + `Transaction refers to unkown output index '${input.txOutIndex}' of transaction '${txOut}'`, + ); + return false; + } - // The public key hash of the used transaction output must correspond - // to the public key the transaction is using in this input - if ( - Hash.fromPublicKey(input.script.publicKey).valueOf() !== - originTransactionOutput.script.publicKeyHash.valueOf() - ) { - console.warn( - "The transaction output public key hash does not correspond to the spender's public key hash", - ); - return false; - } - totalInputAmount += originTransactionOutput.value; + const originTransactionOutput = transactionStates[txOut].outputs[input.txOutIndex]; + + // The public key hash of the used transaction output must correspond + // to the public key the transaction is using in this input + if ( + Hash.fromPublicKey(input.script.publicKey).valueOf() !== + originTransactionOutput.script.publicKeyHash.valueOf() + ) { + console.warn( + "The transaction output public key hash does not correspond to the spender's public key hash", + ); + return false; + } + totalInputAmount += originTransactionOutput.value; - return true; - }); + return true; + }); if (!inputsAreValid) { console.warn('The transaction inputs are not valid'); From 9c3a906898285a78ef0f3974761ddf7e2700df22 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Tue, 7 Feb 2023 17:11:40 +0100 Subject: [PATCH 084/121] Add text feedback when confirm button is disabled on event creation --- .../evoting/screens/CreateElection.tsx | 18 ++++++++++++++++++ .../features/meeting/screens/CreateMeeting.tsx | 11 +++++++++++ .../rollCall/screens/CreateRollCall.tsx | 16 ++++++++++++++++ fe1-web/src/resources/strings.ts | 10 ++++++++-- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/fe1-web/src/features/evoting/screens/CreateElection.tsx b/fe1-web/src/features/evoting/screens/CreateElection.tsx index 2f7a94cf34..24fcec2932 100644 --- a/fe1-web/src/features/evoting/screens/CreateElection.tsx +++ b/fe1-web/src/features/evoting/screens/CreateElection.tsx @@ -285,6 +285,24 @@ const CreateElection = () => { {STRINGS.election_create_add_question} + {!isConnected && ( + + {STRINGS.event_creation_must_be_connected} + + )} + {electionName === '' && ( + + {STRINGS.event_creation_name_not_empty} + + )} + {questions.some(isQuestionInvalid) && ( + + {STRINGS.election_create_invalid_questions_1 + + MIN_BALLOT_OPTIONS + + STRINGS.election_create_invalid_questions_2} + + )} + { placeholder={STRINGS.meeting_create_location_placeholder} /> + {!isConnected && ( + + {STRINGS.event_creation_must_be_connected} + + )} + {meetingName === '' && ( + + {STRINGS.event_creation_name_not_empty} + + )} + { {/* see archive branches for date picker used for native apps */} {Platform.OS === 'web' && buildDatePickerWeb()} + {!isConnected && ( + + {STRINGS.event_creation_must_be_connected} + + )} + {rollCallName === '' && ( + + {STRINGS.event_creation_name_not_empty} + + )} + {rollCallLocation === '' && ( + + {STRINGS.event_creation_location_not_empty} + + )} + Date: Tue, 7 Feb 2023 17:56:19 +0100 Subject: [PATCH 085/121] Add tests --- .../screens/__tests__/CreateElection.test.tsx | 17 +- .../CreateElection.test.tsx.snap | 895 +++++++++++- .../meeting/screens/CreateMeeting.tsx | 1 + .../screens/__tests__/CreateMeeting.test.tsx | 16 +- .../__snapshots__/CreateMeeting.test.tsx.snap | 598 +++++++- .../screens/__tests__/CreateRollCall.test.tsx | 39 +- .../CreateRollCall.test.tsx.snap | 1298 ++++++++++++++++- 7 files changed, 2847 insertions(+), 17 deletions(-) diff --git a/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx b/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx index a98cf55271..9b94552a03 100644 --- a/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx +++ b/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx @@ -1,5 +1,6 @@ -import { render } from '@testing-library/react-native'; +import { fireEvent, render } from '@testing-library/react-native'; import React from 'react'; +import { act } from 'react-test-renderer'; import MockNavigator from '__tests__/components/MockNavigator'; import { @@ -31,7 +32,19 @@ const contextValue = { }; describe('CreateElection', () => { - it('renders correctly', () => { + it('renders correctly when question is empty', () => { + const { getByTestId, toJSON } = render( + + + , + ); + + const nameInput = getByTestId('election_name_selector'); + fireEvent.changeText(nameInput, 'myElection'); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly when name and question are empty', () => { const component = render( diff --git a/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/CreateElection.test.tsx.snap b/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/CreateElection.test.tsx.snap index 6662dc8ec6..3532b5f4a5 100644 --- a/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/CreateElection.test.tsx.snap +++ b/fe1-web/src/features/evoting/screens/__tests__/__snapshots__/CreateElection.test.tsx.snap @@ -1,6 +1,879 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CreateElection renders correctly 1`] = ` +exports[`CreateElection renders correctly when name and question are empty 1`] = ` + + + + + + + + + + + + + + + MockScreen + + + + + + + + + + + + + + + + + + + + + Name + + + + + + Election Type + + + + + + + Question + + 1 + + + + + + Ballot Options + + + + + + + + + + + {"name":"ios-trash","size":25,"color":"#000"} + + + + + + + + + + + + + Add Question + + + + + + + Name must not be empty. + + + All questions must have a title and at least 2 different ballot options. + + + + + + + + + + + + Confirm + + + + + + + + + + + + + + + + + + +`; + +exports[`CreateElection renders correctly when question is empty 1`] = ` + + All questions must have a title and at least 2 different ballot options. + { value={meetingName} onChange={setMeetingName} placeholder={STRINGS.meeting_create_name_placeholder} + testID="meeting_name_selector" /> {/* see archive branches for date picker used for native apps */} diff --git a/fe1-web/src/features/meeting/screens/__tests__/CreateMeeting.test.tsx b/fe1-web/src/features/meeting/screens/__tests__/CreateMeeting.test.tsx index 3569b0cd9a..9d3562a23e 100644 --- a/fe1-web/src/features/meeting/screens/__tests__/CreateMeeting.test.tsx +++ b/fe1-web/src/features/meeting/screens/__tests__/CreateMeeting.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react-native'; +import { fireEvent, render } from '@testing-library/react-native'; import React from 'react'; import MockNavigator from '__tests__/components/MockNavigator'; @@ -16,7 +16,19 @@ const contextValue = { }; describe('CreateMeeting', () => { - it('renders correctly', () => { + it('renders correctly when name is not empty', () => { + const { getByTestId, toJSON } = render( + + + , + ); + + const nameInput = getByTestId('meeting_name_selector'); + fireEvent.changeText(nameInput, 'myMeeting'); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly when name is empty', () => { const component = render( diff --git a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap index c879d4637d..061b83a05e 100644 --- a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap +++ b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CreateMeeting renders correctly 1`] = ` +exports[`CreateMeeting renders correctly when name is empty 1`] = ` @@ -455,6 +456,24 @@ exports[`CreateMeeting renders correctly 1`] = ` value="" /> + + Name must not be empty. + `; + +exports[`CreateMeeting renders correctly when name is not empty 1`] = ` + + + + + + + + + + + + + + + MockScreen + + + + + + + + + + + + + + + + + + + + + Name + + + + + + Location + + + + + + + + + + + + + + + Create meeting + + + + + + + + + + + + + + + + + + +`; diff --git a/fe1-web/src/features/rollCall/screens/__tests__/CreateRollCall.test.tsx b/fe1-web/src/features/rollCall/screens/__tests__/CreateRollCall.test.tsx index 69276a0a5c..9f5874d2c9 100644 --- a/fe1-web/src/features/rollCall/screens/__tests__/CreateRollCall.test.tsx +++ b/fe1-web/src/features/rollCall/screens/__tests__/CreateRollCall.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react-native'; +import { fireEvent, render } from '@testing-library/react-native'; import React from 'react'; import MockNavigator from '__tests__/components/MockNavigator'; @@ -20,12 +20,41 @@ const contextValue = { }; describe('CreateRollCall', () => { - it('renders correctly', () => { - const component = render( + it('renders correctly when name is empty', () => { + const { getByTestId, toJSON } = render( , - ).toJSON(); - expect(component).toMatchSnapshot(); + ); + + const locationInput = getByTestId('roll_call_location_selector'); + fireEvent.changeText(locationInput, 'EPFL'); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly when location is empty', () => { + const { getByTestId, toJSON } = render( + + + , + ); + + const nameInput = getByTestId('roll_call_name_selector'); + fireEvent.changeText(nameInput, 'myRollCall'); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly when location and name are not empty', () => { + const { getByTestId, toJSON } = render( + + + , + ); + + const nameInput = getByTestId('roll_call_name_selector'); + const locationInput = getByTestId('roll_call_location_selector'); + fireEvent.changeText(nameInput, 'myRollCall'); + fireEvent.changeText(locationInput, 'EPFL'); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/fe1-web/src/features/rollCall/screens/__tests__/__snapshots__/CreateRollCall.test.tsx.snap b/fe1-web/src/features/rollCall/screens/__tests__/__snapshots__/CreateRollCall.test.tsx.snap index ec8ae33067..710a45a774 100644 --- a/fe1-web/src/features/rollCall/screens/__tests__/__snapshots__/CreateRollCall.test.tsx.snap +++ b/fe1-web/src/features/rollCall/screens/__tests__/__snapshots__/CreateRollCall.test.tsx.snap @@ -1,6 +1,1282 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CreateRollCall renders correctly 1`] = ` +exports[`CreateRollCall renders correctly when location and name are not empty 1`] = ` + + + + + + + + + + + + + + + MockScreen + + + + + + + + + + + + + + + + + + + + + Name + + + + + + Location + + + + + + Description + + + + + + + + + + + + + + + Confirm + + + + + + + + + + + + + + + + + + +`; + +exports[`CreateRollCall renders correctly when location is empty 1`] = ` + + + + + + + + + + + + + + + MockScreen + + + + + + + + + + + + + + + + + + + + + Name + + + + + + Location + + + + + + Description + + + + + + Location must not be empty. + + + + + + + + + + + + Confirm + + + + + + + + + + + + + + + + + + +`; + +exports[`CreateRollCall renders correctly when name is empty 1`] = ` + + Name must not be empty. + Date: Tue, 7 Feb 2023 23:46:46 +0100 Subject: [PATCH 086/121] reformat code --- be2-scala/project/build.properties | 2 +- .../graph/validators/MessageValidator.scala | 15 ++++- .../graph/validators/RollCallValidator.scala | 67 +++++++++++++++---- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/be2-scala/project/build.properties b/be2-scala/project/build.properties index dd4ff4368b..3161d2146c 100644 --- a/be2-scala/project/build.properties +++ b/be2-scala/project/build.properties @@ -1 +1 @@ -sbt.version = 1.6.1 +sbt.version=1.6.1 diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index 62dc07d2b3..4fb0c72d34 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -48,7 +48,10 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } def checkAttendee(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { - if (validateAttendee(sender, channel, dbActor)) Left(rpcMessage) else Right(error) + if (validateAttendee(sender, channel, dbActor)) + Left(rpcMessage) + else + Right(error) } /** checks whether the sender of the JsonRpcRequest is the LAO owner @@ -69,7 +72,10 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } def checkOwner(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { - if (validateOwner(sender, channel, dbActor)) Left(rpcMessage) else Right(error) + if (validateOwner(sender, channel, dbActor)) + Left(rpcMessage) + else + Right(error) } /** checks whether the channel of the JsonRpcRequest is of the given type @@ -90,7 +96,10 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } def checkChannelType(rpcMessage: JsonRpcRequest, channelObjectType: ObjectType.ObjectType, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { - if (validateChannelType(channelObjectType, channel, dbActor)) Left(rpcMessage) else Right(error) + if (validateChannelType(channelObjectType, channel, dbActor)) + Left(rpcMessage) + else + Right(error) } def extractData[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index d5c2820c3c..d8bd48905b 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -1,12 +1,12 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef -import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} +import ch.epfl.pop.model.network.{JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message -import ch.epfl.pop.model.network.method.message.data.ActionType.{ActionType, CLOSE, CREATE, OPEN, REOPEN} -import ch.epfl.pop.model.network.method.message.data.{ActionType, ObjectType} +import ch.epfl.pop.model.network.method.message.data.ActionType.{CLOSE, CREATE, OPEN, REOPEN} +import ch.epfl.pop.model.network.method.message.data.{ObjectType} import ch.epfl.pop.model.network.method.message.data.rollCall.{CloseRollCall, CreateRollCall, IOpenRollCall} -import ch.epfl.pop.model.objects.{Channel, Hash, PublicKey, RollCallData} +import ch.epfl.pop.model.objects.{Hash, RollCallData} import ch.epfl.pop.pubsub.graph.validators.MessageValidator._ import ch.epfl.pop.pubsub.graph.{GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor @@ -69,11 +69,19 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage, data.proposed_start, data.proposed_end, - validationError(s"'proposed_end' (${data.proposed_end}) timestamp is smaller than 'proposed_start' (${data.proposed_start})") + validationError( + s"'proposed_end' (${data.proposed_end}) timestamp is smaller than 'proposed_start' (${data.proposed_start})" + ) ), checkId(rpcMessage, expectedRollCallId, data.id, validationError(s"unexpected id")), checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), - checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a CreateRollCall message on a wrong type of channel $channel")) + checkChannelType( + rpcMessage, + ObjectType.LAO, + channel, + dbActorRef, + validationError(s"trying to send a CreateRollCall message on a wrong type of channel $channel") + ) )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } @@ -100,10 +108,20 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa ) runList(List( - checkTimestampStaleness(rpcMessage, data.opened_at, validationError(s"stale 'opened_at' timestamp (${data.opened_at})")), + checkTimestampStaleness( + rpcMessage, + data.opened_at, + validationError(s"stale 'opened_at' timestamp (${data.opened_at})") + ), checkId(rpcMessage, expectedRollCallId, data.update_id, validationError("unexpected id 'update_id'")), checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), - checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a $validatorName message on a wrong type of channel $channel")), + checkChannelType( + rpcMessage, + ObjectType.LAO, + channel, + dbActorRef, + validationError(s"trying to send a $validatorName message on a wrong type of channel $channel") + ), validateOpens(rpcMessage, laoId, data.opens, validationError("unexpected id 'opens'")) )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) @@ -114,7 +132,10 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { case Some(data) => - if ((data.state == CREATE || data.state == CLOSE) && data.updateId == opens) Left(rpcMessage) else Right(error) + if ((data.state == CREATE || data.state == CLOSE) && data.updateId == opens) + Left(rpcMessage) + else + Right(error) case _ => Right(error) } } @@ -145,12 +166,23 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa runList(List( checkTimestampStaleness(rpcMessage, data.closed_at, validationError(s"stale 'closed_at' timestamp (${data.closed_at})")), - checkAttendeeSize(rpcMessage, data.attendees.size, data.attendees.toSet.size, validationError("duplicate attendees keys")), + checkAttendeeSize( + rpcMessage, + data.attendees.size, + data.attendees.toSet.size, + validationError("duplicate attendees keys") + ), checkAttendee(rpcMessage, sender, channel, dbActorRef, validationError("unexpected attendees keys")), checkId(rpcMessage, expectedRollCallId, data.update_id, validationError("unexpected id 'update_id'")), validateCloses(rpcMessage, laoId, data.closes, validationError("unexpected id 'closes'")), checkOwner(rpcMessage, sender, channel, dbActorRef, validationError(s"invalid sender $sender")), - checkChannelType(rpcMessage, ObjectType.LAO, channel, dbActorRef, validationError(s"trying to send a CloseRollCall message on a wrong type of channel $channel")) + checkChannelType( + rpcMessage, + ObjectType.LAO, + channel, + dbActorRef, + validationError(s"trying to send a CloseRollCall message on a wrong type of channel $channel") + ) )) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } @@ -159,12 +191,19 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa private def validateCloses(rpcMessage: JsonRpcRequest, laoId: Hash, closes: Hash, error: PipelineError): GraphMessage = { val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { - case Some(data) => if ((data.state == OPEN || data.state == REOPEN) && data.updateId == closes) Left(rpcMessage) else Right(error) - case _ => Right(error) + case Some(data) => + if ((data.state == OPEN || data.state == REOPEN) && data.updateId == closes) + Left(rpcMessage) + else + Right(error) + case _ => Right(error) } } private def checkAttendeeSize(rpcMessage: JsonRpcRequest, size: Int, expectedSize: Int, error: PipelineError): GraphMessage = { - if (size == expectedSize) Left(rpcMessage) else Right(error) + if (size == expectedSize) + Left(rpcMessage) + else + Right(error) } } From 0d231bfcd3802a447a0c1835302768f6f9fcee61 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 7 Feb 2023 23:51:10 +0100 Subject: [PATCH 087/121] reformat code + remove unused import --- .../MessageDataContentValidator.scala | 22 +++++++++++++--- .../graph/validators/MessageValidator.scala | 26 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala index cbb60a7c56..126986a669 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala @@ -25,7 +25,10 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta final def validateTimestampStaleness(timestamp: Timestamp): Boolean = TIMESTAMP_BASE_TIME < timestamp def checkTimestampStaleness(rpcMessage: JsonRpcRequest, timestamp: Timestamp, error: PipelineError): GraphMessage = { - if (validateTimestampStaleness(timestamp)) Left(rpcMessage) else Right(error) + if (validateTimestampStaleness(timestamp)) + Left(rpcMessage) + else + Right(error) } /** Check whether timestamp is not older than timestamp @@ -39,12 +42,23 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta */ final def validateTimestampOrder(first: Timestamp, second: Timestamp): Boolean = first <= second - def checkTimestampOrder(rpcMessage: JsonRpcRequest, first: Timestamp, second: Timestamp, error: PipelineError): GraphMessage = { - if (validateTimestampOrder(first, second)) Left(rpcMessage) else Right(error) + def checkTimestampOrder( + rpcMessage: JsonRpcRequest, + first: Timestamp, + second: Timestamp, + error: PipelineError + ): GraphMessage = { + if (validateTimestampOrder(first, second)) + Left(rpcMessage) + else + Right(error) } def checkId(rpcMessage: JsonRpcRequest, expectedId: Hash, id: Hash, error: PipelineError): GraphMessage = { - if (expectedId == id) Left(rpcMessage) else Right(error) + if (expectedId == id) + Left(rpcMessage) + else + Right(error) } /** Check whether a list of public keys are valid or not diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index 4fb0c72d34..f3173de887 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -1,7 +1,7 @@ package ch.epfl.pop.pubsub.graph.validators import akka.pattern.AskableActorRef -import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} +import ch.epfl.pop.model.network.{JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.ObjectType import ch.epfl.pop.model.objects.{Channel, Hash, PublicKey} @@ -47,7 +47,13 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } - def checkAttendee(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + def checkAttendee( + rpcMessage: JsonRpcRequest, + sender: PublicKey, + channel: Channel, + dbActor: AskableActorRef = DbActor.getInstance, + error: PipelineError + ): GraphMessage = { if (validateAttendee(sender, channel, dbActor)) Left(rpcMessage) else @@ -71,7 +77,13 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } - def checkOwner(rpcMessage: JsonRpcRequest, sender: PublicKey, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + def checkOwner( + rpcMessage: JsonRpcRequest, + sender: PublicKey, + channel: Channel, + dbActor: AskableActorRef = DbActor.getInstance, + error: PipelineError + ): GraphMessage = { if (validateOwner(sender, channel, dbActor)) Left(rpcMessage) else @@ -95,7 +107,13 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } - def checkChannelType(rpcMessage: JsonRpcRequest, channelObjectType: ObjectType.ObjectType, channel: Channel, dbActor: AskableActorRef = DbActor.getInstance, error: PipelineError): GraphMessage = { + def checkChannelType( + rpcMessage: JsonRpcRequest, + channelObjectType: ObjectType.ObjectType, + channel: Channel, + dbActor: AskableActorRef = DbActor.getInstance, + error: PipelineError + ): GraphMessage = { if (validateChannelType(channelObjectType, channel, dbActor)) Left(rpcMessage) else From 6239db9922997b6b598537c370f3b7baae31c9b3 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 7 Feb 2023 23:53:22 +0100 Subject: [PATCH 088/121] reformat code --- .../pubsub/graph/validators/RollCallValidator.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index d8bd48905b..c6654dc2b6 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -55,7 +55,12 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa rpcMessage.getParamsMessage match { case Some(message: Message) => val (data, laoId, sender, channel) = extractData[CreateRollCall](rpcMessage) - val expectedRollCallId: Hash = Hash.fromStrings(EVENT_HASH_PREFIX, laoId.toString, data.creation.toString, data.name) + val expectedRollCallId: Hash = Hash.fromStrings( + EVENT_HASH_PREFIX, + laoId.toString, + data.creation.toString, + data.name + ) runList(List( checkTimestampStaleness(rpcMessage, data.creation, validationError(s"stale 'creation' timestamp (${data.creation})")), @@ -165,7 +170,11 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa ) runList(List( - checkTimestampStaleness(rpcMessage, data.closed_at, validationError(s"stale 'closed_at' timestamp (${data.closed_at})")), + checkTimestampStaleness( + rpcMessage, + data.closed_at, + validationError(s"stale 'closed_at' timestamp (${data.closed_at})") + ), checkAttendeeSize( rpcMessage, data.attendees.size, From 7e7c2418beaad5d7d2778f7209845e73eccd3e16 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 8 Feb 2023 00:07:43 +0100 Subject: [PATCH 089/121] change name runChecks --- .../epfl/pop/pubsub/graph/validators/MessageValidator.scala | 4 ++-- .../pop/pubsub/graph/validators/RollCallValidator.scala | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index f3173de887..4ae3e29b23 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -130,9 +130,9 @@ object MessageValidator extends ContentValidator with AskPatternConstants { (data, laoId, sender, channel) } - def runList(list: List[GraphMessage]): GraphMessage = { + def runChecks(list: List[GraphMessage]): GraphMessage = { if (list.head.isLeft && !list.tail.isEmpty) - runList(list.tail) + runChecks(list.tail) else list.head } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index c6654dc2b6..2e24c12cbe 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -62,7 +62,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.name ) - runList(List( + runChecks(List( checkTimestampStaleness(rpcMessage, data.creation, validationError(s"stale 'creation' timestamp (${data.creation})")), checkTimestampOrder( rpcMessage, @@ -112,7 +112,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.opened_at.toString ) - runList(List( + runChecks(List( checkTimestampStaleness( rpcMessage, data.opened_at, @@ -169,7 +169,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.closed_at.toString ) - runList(List( + runChecks(List( checkTimestampStaleness( rpcMessage, data.closed_at, From 1414e101fa8f08ead37efb91a193f7863a56b133 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 8 Feb 2023 00:17:20 +0100 Subject: [PATCH 090/121] dbBroadcast generic --- .../pop/pubsub/graph/handlers/MessageHandler.scala | 9 +++++---- .../pubsub/graph/handlers/SocialMediaHandler.scala | 11 ++--------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala index 5629f6eec1..6fc95f2dae 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala @@ -92,7 +92,7 @@ trait MessageHandler extends AskPatternConstants { * @return * the database answer wrapped in a [[scala.concurrent.Future]] */ - def dbBroadcast(rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: Base64Data, broadcastChannel: Channel): Future[GraphMessage] = { + def dbBroadcast[T](rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: T, broadcastChannel: Channel): Future[GraphMessage] = { val m: Message = rpcMessage.getParamsMessage.getOrElse( return Future { Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"dbAskWritePropagate failed : retrieve empty rpcRequest message", rpcMessage.id)) @@ -101,9 +101,10 @@ trait MessageHandler extends AskPatternConstants { val combined = for { DbActorReadLaoDataAck(laoData) <- dbActor ? DbActor.ReadLaoData(channel) - broadcastSignature: Signature = laoData.privateKey.signData(broadcastData) - broadcastId: Hash = Hash.fromStrings(broadcastData.toString, broadcastSignature.toString) - broadcastMessage: Message = Message(broadcastData, laoData.publicKey, broadcastSignature, broadcastId, List.empty) + encodedData: Base64Data = Base64Data.encode(broadcastData.toString) + broadcastSignature: Signature = laoData.privateKey.signData(encodedData) + broadcastId: Hash = Hash.fromStrings(encodedData.toString, broadcastSignature.toString) + broadcastMessage: Message = Message(encodedData, laoData.publicKey, broadcastSignature, broadcastId, List.empty) _ <- dbActor ? DbActor.WriteAndPropagate(broadcastChannel, broadcastMessage) } yield () diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index e2e4f15437..f761762927 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -40,13 +40,6 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { private def generateSocialChannel(lao_id: Hash): Channel = Channel(Channel.ROOT_CHANNEL_PREFIX + lao_id + Channel.SOCIAL_MEDIA_CHIRPS_PREFIX) - // the following function encodes the data and broadcast it - private def broadcastData[T](rpcMessage: JsonRpcRequest, data: T, channel: Channel, broadcastChannel: Channel): GraphMessage = { - val encodedData: Base64Data = Base64Data.encode(data.toString) - val broadcast: Future[GraphMessage] = dbBroadcast(rpcMessage, channel, encodedData, broadcastChannel) - Await.result(broadcast, duration) - } - def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = for { @@ -59,7 +52,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) - broadcastData[NotifyAddChirp](rpcMessage, notifyAddChirp, channelChirp, broadcastChannel) + Await.result(dbBroadcast[NotifyAddChirp](rpcMessage, channelChirp, notifyAddChirp, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } @@ -77,7 +70,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) - broadcastData[NotifyDeleteChirp](rpcMessage, notifyDeleteChirp, channelChirp, broadcastChannel) + Await.result(dbBroadcast[NotifyDeleteChirp](rpcMessage, channelChirp, notifyDeleteChirp, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } From 3ba5757fb1ab569424cb688bdd2ea5c318baee79 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 8 Feb 2023 00:17:58 +0100 Subject: [PATCH 091/121] unused imports --- .../ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala index 6fc95f2dae..da81084ae1 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala @@ -3,16 +3,15 @@ package ch.epfl.pop.pubsub.graph.handlers import akka.pattern.AskableActorRef import ch.epfl.pop.model.network.JsonRpcRequest import ch.epfl.pop.model.network.method.message.Message -import ch.epfl.pop.model.objects.{Base64Data, Channel, DbActorNAckException, Hash, Signature} +import ch.epfl.pop.model.objects.{Base64Data, Channel, Hash, Signature} import ch.epfl.pop.pubsub.AskPatternConstants import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor import ch.epfl.pop.storage.DbActor.DbActorReadLaoDataAck -import com.sun.org.apache.xalan.internal.xsltc.compiler.util.ErrorMsg import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Await, Future} -import scala.util.{Failure, Success} +import scala.concurrent.{Future} +import scala.util.{Success} trait MessageHandler extends AskPatternConstants { From f0068600ddae2f00db94c0d10d176a0c1b3267a0 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Wed, 8 Feb 2023 16:03:18 +0100 Subject: [PATCH 092/121] Update snapshots and remove unused import --- .../evoting/screens/__tests__/CreateElection.test.tsx | 1 - .../__snapshots__/CreateMeeting.test.tsx.snap | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx b/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx index 9b94552a03..84af655964 100644 --- a/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx +++ b/fe1-web/src/features/evoting/screens/__tests__/CreateElection.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render } from '@testing-library/react-native'; import React from 'react'; -import { act } from 'react-test-renderer'; import MockNavigator from '__tests__/components/MockNavigator'; import { diff --git a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap index 061b83a05e..1b543e8f8a 100644 --- a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap +++ b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap @@ -276,7 +276,7 @@ exports[`CreateMeeting renders correctly when name is empty 1`] = ` @@ -519,7 +519,7 @@ exports[`CreateMeeting renders correctly when name is empty 1`] = ` From 318f205d8b6a75948117609bdc9d308081fdb2d5 Mon Sep 17 00:00:00 2001 From: NicolasGilliard Date: Thu, 9 Feb 2023 13:34:51 +0100 Subject: [PATCH 093/121] rollback fix tentative --- be1-go/channel/consensus/mod_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/be1-go/channel/consensus/mod_test.go b/be1-go/channel/consensus/mod_test.go index 96351db0ff..2df7c7a9d4 100644 --- a/be1-go/channel/consensus/mod_test.go +++ b/be1-go/channel/consensus/mod_test.go @@ -1476,9 +1476,7 @@ func Test_Timeout_Prepare(t *testing.T) { fakeHub.fakeSock.msg = nil - clock.Add(4 * time.Second) - time.Sleep(500 * time.Millisecond) - clock.Add(4 * time.Second) + clock.Add(12 * time.Second) time.Sleep(500 * time.Millisecond) // A failure message should be sent to the socket From f1fdde01f3d87db34d02a3d9882e0c4fe29fb9fd Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Mon, 13 Feb 2023 16:43:49 +0100 Subject: [PATCH 094/121] Address Nico's comments --- .../evoting/screens/CreateElection.tsx | 19 ++++++++++++++++++- fe1-web/src/resources/strings.ts | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/fe1-web/src/features/evoting/screens/CreateElection.tsx b/fe1-web/src/features/evoting/screens/CreateElection.tsx index 24fcec2932..118f81f618 100644 --- a/fe1-web/src/features/evoting/screens/CreateElection.tsx +++ b/fe1-web/src/features/evoting/screens/CreateElection.tsx @@ -69,6 +69,15 @@ const MIN_BALLOT_OPTIONS = 2; const isQuestionInvalid = (question: NewQuestion): boolean => question.question === '' || question.ballot_options.length < MIN_BALLOT_OPTIONS; +/** + * Checks whether a question title is not unique within a list of questions + * @param questions The list of questions + */ +const haveQuestionsSameTitle = (questions: NewQuestion[]): boolean => { + const questionTitles = questions.map((q: NewQuestion) => q.question); + return questionTitles.length === new Set(questionTitles).size; +}; + /** * Creates a new election based on the given values and returns the related request promise * @param laoId The id of the lao in which the new election should be created @@ -141,7 +150,10 @@ const CreateElection = () => { // Confirm button only clickable when the Name, Question and 2 Ballot options have values const confirmButtonEnabled: boolean = - isConnected === true && electionName !== '' && !questions.some(isQuestionInvalid); + isConnected === true && + electionName !== '' && + !questions.some(isQuestionInvalid) && + haveQuestionsSameTitle(questions); const onCreateElection = () => { createElection(currentLao.id, version, electionName, questions, startTime, endTime) @@ -302,6 +314,11 @@ const CreateElection = () => { STRINGS.election_create_invalid_questions_2} )} + {!haveQuestionsSameTitle(questions) && ( + + {STRINGS.election_create_same_questions} + + )} Date: Mon, 13 Feb 2023 16:59:21 +0100 Subject: [PATCH 095/121] Update snapshot --- .../__tests__/__snapshots__/CreateMeeting.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap index 1b543e8f8a..bf7b9ed95c 100644 --- a/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap +++ b/fe1-web/src/features/meeting/screens/__tests__/__snapshots__/CreateMeeting.test.tsx.snap @@ -276,7 +276,7 @@ exports[`CreateMeeting renders correctly when name is empty 1`] = ` Date: Mon, 13 Feb 2023 17:39:36 +0100 Subject: [PATCH 096/121] Fix persistence bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johann PlΓΌss --- .../popstellar/ui/detail/LaoDetailActivity.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java index 066786f1a5..5820fa59ee 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/detail/LaoDetailActivity.java @@ -70,9 +70,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } @Override - public void onStop() { - super.onStop(); - + /* + Normally the saving routine should be called onStop, such as is done in other activities, + Yet here for unknown reasons the subscriptions set in LAONetworkManager is empty when going + to HomeActivity. This fixes it. Since our persistence is light for now (13.02.2023) - i.e. + server address, wallet seed and channel list - and not computationally intensive this will not + be a problem at the moment + */ + public void onPause() { + super.onPause(); try { viewModel.savePersistentData(); } catch (GeneralSecurityException e) { From 1175370d393b36bfb87f5f1a84734712ff256678 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 18:33:29 +0100 Subject: [PATCH 097/121] changed dbBroadcast function parameter --- .../epfl/pop/pubsub/graph/handlers/ElectionHandler.scala | 9 +++++---- .../epfl/pop/pubsub/graph/handlers/MessageHandler.scala | 4 ++-- .../pop/pubsub/graph/handlers/SocialMediaHandler.scala | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala index b6e89b4892..054620d052 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala @@ -66,8 +66,10 @@ class ElectionHandler(dbRef: => AskableActorRef) extends MessageHandler { case OPEN_BALLOT => Left(rpcMessage) case SECRET_BALLOT => val keyElection: KeyElection = KeyElection(electionId, keyPair.publicKey) - val broadcastKey: Base64Data = Base64Data.encode(KeyElectionFormat.write(keyElection).toString) - Await.result(dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, broadcastKey, electionChannel), duration) + Await.result( + dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, KeyElectionFormat.write(keyElection).toString, electionChannel), + duration + ) } case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleSetupElection failed : ${ex.message}", rpcMessage.getId)) case reply => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"handleSetupElection failed : unexpected DbActor reply '$reply'", rpcMessage.getId)) @@ -113,9 +115,8 @@ class ElectionHandler(dbRef: => AskableActorRef) extends MessageHandler { _ <- dbAskWritePropagate(rpcMessage) // data to be broadcast resultElection: ResultElection = ResultElection(electionQuestionResults, witnessSignatures) - data: Base64Data = Base64Data.encode(resultElectionFormat.write(resultElection).toString) // create & propagate the resultMessage - _ <- dbBroadcast(rpcMessage, electionChannel, data, electionChannel) + _ <- dbBroadcast(rpcMessage, electionChannel, resultElectionFormat.write(resultElection).toString, electionChannel) } yield () Await.ready(combined, duration).value match { case Some(Success(_)) => Left(rpcMessage) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala index da81084ae1..0594d1675d 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala @@ -91,7 +91,7 @@ trait MessageHandler extends AskPatternConstants { * @return * the database answer wrapped in a [[scala.concurrent.Future]] */ - def dbBroadcast[T](rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: T, broadcastChannel: Channel): Future[GraphMessage] = { + def dbBroadcast[T](rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: String, broadcastChannel: Channel): Future[GraphMessage] = { val m: Message = rpcMessage.getParamsMessage.getOrElse( return Future { Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"dbAskWritePropagate failed : retrieve empty rpcRequest message", rpcMessage.id)) @@ -100,7 +100,7 @@ trait MessageHandler extends AskPatternConstants { val combined = for { DbActorReadLaoDataAck(laoData) <- dbActor ? DbActor.ReadLaoData(channel) - encodedData: Base64Data = Base64Data.encode(broadcastData.toString) + encodedData: Base64Data = Base64Data.encode(broadcastData) broadcastSignature: Signature = laoData.privateKey.signData(encodedData) broadcastId: Hash = Hash.fromStrings(encodedData.toString, broadcastSignature.toString) broadcastMessage: Message = Message(encodedData, laoData.publicKey, broadcastSignature, broadcastId, List.empty) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index f761762927..15119a3bb8 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -52,7 +52,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast[NotifyAddChirp](rpcMessage, channelChirp, notifyAddChirp, broadcastChannel), duration) + Await.result(dbBroadcast[NotifyAddChirp](rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } @@ -70,7 +70,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast[NotifyDeleteChirp](rpcMessage, channelChirp, notifyDeleteChirp, broadcastChannel), duration) + Await.result(dbBroadcast[NotifyDeleteChirp](rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } From b69e85e991fee4cc8b51e8f798a124b7d5825bd4 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 18:37:30 +0100 Subject: [PATCH 098/121] reduce line length --- .../pubsub/graph/handlers/MessageHandler.scala | 2 +- .../graph/handlers/SocialMediaHandler.scala | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala index 0594d1675d..701c4e6642 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala @@ -91,7 +91,7 @@ trait MessageHandler extends AskPatternConstants { * @return * the database answer wrapped in a [[scala.concurrent.Future]] */ - def dbBroadcast[T](rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: String, broadcastChannel: Channel): Future[GraphMessage] = { + def dbBroadcast(rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: String, broadcastChannel: Channel): Future[GraphMessage] = { val m: Message = rpcMessage.getParamsMessage.getOrElse( return Future { Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"dbAskWritePropagate failed : retrieve empty rpcRequest message", rpcMessage.id)) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 15119a3bb8..0f6e1c1422 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -2,7 +2,7 @@ package ch.epfl.pop.pubsub.graph.handlers import akka.pattern.AskableActorRef import ch.epfl.pop.json.MessageDataProtocol._ -import ch.epfl.pop.model.network.{JsonRpcMessage, JsonRpcRequest} +import ch.epfl.pop.model.network.{JsonRpcRequest} import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.socialMedia._ import ch.epfl.pop.model.objects._ @@ -52,9 +52,11 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast[NotifyAddChirp](rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) - case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) - case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) + Await.result(dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) + case Some(Failure(ex: DbActorNAckException)) => + Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) + case _ => + Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } } @@ -70,9 +72,11 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast[NotifyDeleteChirp](rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) - case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) - case _ => Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) + Await.result(dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) + case Some(Failure(ex: DbActorNAckException)) => + Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) + case _ => + Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) } } From ccb53d8bd65182c1f18649b8ccadf9420bdd0eb8 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 18:38:16 +0100 Subject: [PATCH 099/121] remove unused comments --- .../ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala index 054620d052..d66cd8684f 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala @@ -13,7 +13,6 @@ import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor import ch.epfl.pop.storage.DbActor.DbActorReadElectionDataAck -import java.nio.ByteBuffer import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Await, Future} From a7ae3550727b4f9c7f0a1851bfe05988dc699e82 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 18:50:39 +0100 Subject: [PATCH 100/121] add comments --- .../pop/pubsub/graph/handlers/SocialMediaHandler.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 0f6e1c1422..d2d1009131 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -51,6 +51,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) + // create and propagate the notifyAddChirp message val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) Await.result(dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => @@ -71,6 +72,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) + // create and propagate the notifyDeleteChirp message val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) Await.result(dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) case Some(Failure(ex: DbActorNAckException)) => @@ -99,7 +101,13 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { Await.result(ask, duration) } - // generates the parameters that will be used to broadcast the chirps + /** Helper function that extracts the useful parameters from the message + * + * @param rpcMessage + * : message from which we extract the parameters + * @return + * the id of the chirp, the channel, the decoded data and the channel in which we broadcast + */ private def parametersToBroadcast[T](rpcMessage: JsonRpcRequest): (Hash, Channel, T, Channel) = { val channelChirp: Channel = rpcMessage.getParamsChannel val lao_id: Hash = channelChirp.decodeChannelLaoId.get From a144bde998eb764c6341f428be4913e9fa04417d Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 19:49:57 +0100 Subject: [PATCH 101/121] add comments to ElectionHandler --- .../graph/handlers/ElectionHandler.scala | 14 ++++++++ .../pubsub/graph/handlers/LaoHandler.scala | 3 +- .../graph/handlers/SocialMediaHandler.scala | 32 ++++++++++++++----- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala index d66cd8684f..1fe986403c 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala @@ -123,28 +123,42 @@ class ElectionHandler(dbRef: => AskableActorRef) extends MessageHandler { } } + /** Helper function to create the list of ElectionQuestionResult + * + * @param electionChannel + * : the Channel in which we read the data + * @return + * the list of ElectionQuestionResult wrapped in a [[scala.concurrent.Future]] + */ private def createElectionQuestionResults(electionChannel: Channel): Future[List[ElectionQuestionResult]] = { for { + // get the last votes of the CastVotes messages castsVotesElections <- electionChannel.getLastVotes(dbActor) + // get the setupElection message of the channel setupMessage <- electionChannel.getSetupMessage(dbActor) + // associate the questions ids to their ballots questionToBallots = setupMessage.questions.map(question => question.id -> question.ballot_options).toMap DbActorReadElectionDataAck(electionData) <- dbActor ? DbActor.ReadElectionData(setupMessage.id) } yield { + // set up the table of results val resultsTable = mutable.HashMap.from(for { (question, ballots) <- questionToBallots } yield question -> ballots.map(_ -> 0).toMap) for { castVoteElection <- castsVotesElections voteElection <- castVoteElection.votes + // get the index of the vote voteIndex = electionChannel.getVoteIndex(electionData, voteElection.vote) } { val question = voteElection.question val ballots = questionToBallots(question).toArray val ballot = ballots.apply(voteIndex) val questionResult = resultsTable(question) + // update the results by adding a vote to the corresponding ballot resultsTable.update(question, questionResult.updated(ballot, questionResult(ballot) + 1)) } (for { + // from the results saved in resultsTable, we construct the ElectionQuestionResult (qid, ballotToCount) <- resultsTable electionBallotVotes = List.from(for { (ballot, count) <- ballotToCount diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala index b0b8bb975d..ee175d1b72 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala @@ -53,8 +53,7 @@ case object LaoHandler extends MessageHandler { _ <- dbActor ? DbActor.WriteLaoData(laoChannel, message, address) // after creating the lao, we need to send a lao#greet message to the frontend greet: GreetLao = GreetLao(data.id, params.get.sender, address.get, List.empty) - broadcastGreet: Base64Data = Base64Data.encode(GreetLaoFormat.write(greet).toString()) - _ <- dbBroadcast(rpcMessage, laoChannel, broadcastGreet, laoChannel) + _ <- dbBroadcast(rpcMessage, laoChannel, GreetLaoFormat.write(greet).toString(), laoChannel) } yield () Await.ready(combined, duration).value.get match { diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index d2d1009131..d779ed4443 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -54,10 +54,18 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { // create and propagate the notifyAddChirp message val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) Await.result(dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) - case Some(Failure(ex: DbActorNAckException)) => - Right(PipelineError(ex.code, s"handleAddChirp failed : ${ex.message}", rpcMessage.getId)) - case _ => - Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) + + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError( + ex.code, + s"handleAddChirp failed : ${ex.message}", + rpcMessage.getId + )) + + case _ => Right(PipelineError( + ErrorCodes.SERVER_ERROR.id, + unknownAnswerDatabase, + rpcMessage.getId + )) } } @@ -75,10 +83,18 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { // create and propagate the notifyDeleteChirp message val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) Await.result(dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) - case Some(Failure(ex: DbActorNAckException)) => - Right(PipelineError(ex.code, s"handleDeleteChirp failed : ${ex.message}", rpcMessage.getId)) - case _ => - Right(PipelineError(ErrorCodes.SERVER_ERROR.id, unknownAnswerDatabase, rpcMessage.getId)) + + case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError( + ex.code, + s"handleDeleteChirp failed : ${ex.message}", + rpcMessage.getId + )) + + case _ => Right(PipelineError( + ErrorCodes.SERVER_ERROR.id, + unknownAnswerDatabase, + rpcMessage.getId + )) } } From 43e76b9d201c4029d6065e28a96ed131757c6430 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 20:01:49 +0100 Subject: [PATCH 102/121] reduce line lentgh --- .../pop/pubsub/graph/handlers/SocialMediaHandler.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index d779ed4443..aa1a05f960 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -53,7 +53,10 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) // create and propagate the notifyAddChirp message val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration) + Await.result( + dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), + duration + ) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError( ex.code, @@ -82,7 +85,10 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) // create and propagate the notifyDeleteChirp message val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) - Await.result(dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration) + Await.result( + dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), + duration + ) case Some(Failure(ex: DbActorNAckException)) => Right(PipelineError( ex.code, From dd42af93d0645d5a98da1a756d4b35d4534e1805 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 21:10:34 +0100 Subject: [PATCH 103/121] add comments --- .../MessageDataContentValidator.scala | 15 ++++++ .../graph/validators/MessageValidator.scala | 51 ++++++++++++------- .../graph/validators/RollCallValidator.scala | 49 +++++++++++++++++- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala index 126986a669..2f83b046c8 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageDataContentValidator.scala @@ -24,6 +24,7 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta */ final def validateTimestampStaleness(timestamp: Timestamp): Boolean = TIMESTAMP_BASE_TIME < timestamp + // Same as validateTimestampStaleness except that it returns a GraphMessage def checkTimestampStaleness(rpcMessage: JsonRpcRequest, timestamp: Timestamp, error: PipelineError): GraphMessage = { if (validateTimestampStaleness(timestamp)) Left(rpcMessage) @@ -42,6 +43,7 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta */ final def validateTimestampOrder(first: Timestamp, second: Timestamp): Boolean = first <= second + // Same as validateTimestampOrder except that it returns a GraphMessage def checkTimestampOrder( rpcMessage: JsonRpcRequest, first: Timestamp, @@ -54,6 +56,19 @@ trait MessageDataContentValidator extends ContentValidator with AskPatternConsta Right(error) } + /** Checks if the id corresponds to the expected id + * + * @param rpcMessage + * rpc message to check + * @param expectedId + * expected id + * @param id + * id to check + * @param error + * the error to forward in case the id is not the same as the expected id + * @return + * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error + */ def checkId(rpcMessage: JsonRpcRequest, expectedId: Hash, id: Hash, error: PipelineError): GraphMessage = { if (expectedId == id) Left(rpcMessage) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index 4ae3e29b23..acfd9b24d3 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -14,6 +14,37 @@ import scala.util.Success object MessageValidator extends ContentValidator with AskPatternConstants { + /** Extracts the useful parameters from a rpc message + * + * @param rpcMessage + * rpc message which contains the parameters to extract + * @return + * the decoded data, the id of the LAO, the PublicKey of the sender and the channel + */ + def extractData[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { + val message: Message = rpcMessage.getParamsMessage.get + val data: T = message.decodedData.get.asInstanceOf[T] + val laoId: Hash = rpcMessage.extractLaoId + val sender: PublicKey = message.sender + val channel: Channel = rpcMessage.getParamsChannel + + (data, laoId, sender, channel) + } + + /** Runs the multiple checks of the validators + * + * @param list + * List of checks which return a GraphMessage + * @return + * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error + */ + def runChecks(list: List[GraphMessage]): GraphMessage = { + if (list.head.isLeft && !list.tail.isEmpty) + runChecks(list.tail) + else + list.head + } + def validateMessage(rpcMessage: JsonRpcRequest): GraphMessage = { val message: Message = rpcMessage.getParamsMessage.get @@ -47,6 +78,7 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } + // Same as validateAttendee except that it returns a GraphMessage def checkAttendee( rpcMessage: JsonRpcRequest, sender: PublicKey, @@ -77,6 +109,7 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } + // Same as validateOwner except that it returns a GraphMessage def checkOwner( rpcMessage: JsonRpcRequest, sender: PublicKey, @@ -107,6 +140,7 @@ object MessageValidator extends ContentValidator with AskPatternConstants { } } + // Same as validateChannelType except that it returns a GraphMessage def checkChannelType( rpcMessage: JsonRpcRequest, channelObjectType: ObjectType.ObjectType, @@ -119,21 +153,4 @@ object MessageValidator extends ContentValidator with AskPatternConstants { else Right(error) } - - def extractData[T](rpcMessage: JsonRpcRequest): (T, Hash, PublicKey, Channel) = { - val message: Message = rpcMessage.getParamsMessage.get - val data: T = message.decodedData.get.asInstanceOf[T] - val laoId: Hash = rpcMessage.extractLaoId - val sender: PublicKey = message.sender - val channel: Channel = rpcMessage.getParamsChannel - - (data, laoId, sender, channel) - } - - def runChecks(list: List[GraphMessage]): GraphMessage = { - if (list.head.isLeft && !list.tail.isEmpty) - runChecks(list.tail) - else - list.head - } } diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index 2e24c12cbe..9731238f27 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -63,12 +63,18 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa ) runChecks(List( - checkTimestampStaleness(rpcMessage, data.creation, validationError(s"stale 'creation' timestamp (${data.creation})")), + checkTimestampStaleness( + rpcMessage, + data.creation, + validationError(s"stale 'creation' timestamp (${data.creation})") + ), checkTimestampOrder( rpcMessage, data.creation, data.proposed_start, - validationError(s"'proposed_start' (${data.proposed_start}) timestamp is smaller than 'creation' (${data.creation})") + validationError( + s"'proposed_start' (${data.proposed_start}) timestamp is smaller than 'creation' (${data.creation})" + ) ), checkTimestampOrder( rpcMessage, @@ -133,6 +139,19 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa } } + /** Validates the opens id of a OpenRollCAll message + * + * @param rpcMessage + * rpc message to validate + * @param laoId + * id of the LAO to which the rollCallDara belongs + * @param opens + * opens id of the OpenRollCall which needs to be checked + * @param error + * the error to forward in case the opens id does not correspond to the expected id + * @return + * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error + */ private def validateOpens(rpcMessage: JsonRpcRequest, laoId: Hash, opens: Hash, error: PipelineError): GraphMessage = { val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { @@ -197,6 +216,19 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa } } + /** Validates the closes id of CloseRollCall message + * + * @param rpcMessage + * rpc message to validate + * @param laoId + * id of the LAO to which the rollCallDara belongs + * @param closes + * closes id of CloseRollCall message which needs to be checked + * @param error + * the error to forward in case the closes id does not correspond to the expected id + * @return + * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error + */ private def validateCloses(rpcMessage: JsonRpcRequest, laoId: Hash, closes: Hash, error: PipelineError): GraphMessage = { val rollCallData: Option[RollCallData] = getRollCallData(laoId) rollCallData match { @@ -209,6 +241,19 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa } } + /** Checks if the number of attendees is as expected + * + * @param rpcMessage + * rpc message to validate + * @param size + * size of the actual list of attendees + * @param expectedSize + * expected size of attendees + * @param error + * the error to forward in case the size is not as expected + * @return + * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error + */ private def checkAttendeeSize(rpcMessage: JsonRpcRequest, size: Int, expectedSize: Int, error: PipelineError): GraphMessage = { if (size == expectedSize) Left(rpcMessage) From 6f1c2aa51b61f2e6082fbbb833a9637dbe63caa0 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Mon, 13 Feb 2023 22:32:25 +0100 Subject: [PATCH 104/121] variable parameters runChecks --- .../pubsub/graph/validators/MessageValidator.scala | 12 ++++++------ .../pubsub/graph/validators/RollCallValidator.scala | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala index acfd9b24d3..a11d12dc67 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/MessageValidator.scala @@ -33,16 +33,16 @@ object MessageValidator extends ContentValidator with AskPatternConstants { /** Runs the multiple checks of the validators * - * @param list - * List of checks which return a GraphMessage + * @param checks + * checks which return a GraphMessage * @return * GraphMessage: passes the rpcMessages to Left if successful right with pipeline error */ - def runChecks(list: List[GraphMessage]): GraphMessage = { - if (list.head.isLeft && !list.tail.isEmpty) - runChecks(list.tail) + def runChecks(checks: GraphMessage*): GraphMessage = { + if (checks.head.isLeft && !checks.tail.isEmpty) + runChecks(checks.tail: _*) else - list.head + checks.head } def validateMessage(rpcMessage: JsonRpcRequest): GraphMessage = { diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala index 9731238f27..5981874b59 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/validators/RollCallValidator.scala @@ -62,7 +62,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.name ) - runChecks(List( + runChecks( checkTimestampStaleness( rpcMessage, data.creation, @@ -93,7 +93,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa dbActorRef, validationError(s"trying to send a CreateRollCall message on a wrong type of channel $channel") ) - )) + ) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } @@ -118,7 +118,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.opened_at.toString ) - runChecks(List( + runChecks( checkTimestampStaleness( rpcMessage, data.opened_at, @@ -134,7 +134,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa validationError(s"trying to send a $validatorName message on a wrong type of channel $channel") ), validateOpens(rpcMessage, laoId, data.opens, validationError("unexpected id 'opens'")) - )) + ) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } @@ -188,7 +188,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa data.closed_at.toString ) - runChecks(List( + runChecks( checkTimestampStaleness( rpcMessage, data.closed_at, @@ -211,7 +211,7 @@ sealed class RollCallValidator(dbActorRef: => AskableActorRef) extends MessageDa dbActorRef, validationError(s"trying to send a CloseRollCall message on a wrong type of channel $channel") ) - )) + ) case _ => Right(validationErrorNoMessage(rpcMessage.id)) } } From 057981f55f5a169ce6f6346c4ef51fd0c1484384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johann=20Pl=C3=BCss?= Date: Tue, 14 Feb 2023 12:10:38 +0100 Subject: [PATCH 105/121] Adapt diagram --- .../docs/images/architecture_diagram.drawio | 2 +- .../docs/images/architecture_diagram.drawio.png | Bin 0 -> 52231 bytes .../docs/images/architecture_diagram.drawio.svg | 4 ++++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 fe2-android/docs/images/architecture_diagram.drawio.png create mode 100644 fe2-android/docs/images/architecture_diagram.drawio.svg diff --git a/fe2-android/docs/images/architecture_diagram.drawio b/fe2-android/docs/images/architecture_diagram.drawio index 800bab7cf5..9f5ca149e0 100644 --- a/fe2-android/docs/images/architecture_diagram.drawio +++ b/fe2-android/docs/images/architecture_diagram.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/fe2-android/docs/images/architecture_diagram.drawio.png b/fe2-android/docs/images/architecture_diagram.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..ee91226cd6b1e9fe513efe3739d651cecdf7b990 GIT binary patch literal 52231 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
    2$9oRsFuJ-$DPD0mdhVxFiACBixU$5Q^ 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 literal 0 HcmV?d00001 diff --git a/fe2-android/docs/images/architecture_diagram.drawio.svg b/fe2-android/docs/images/architecture_diagram.drawio.svg new file mode 100644 index 0000000000..d18c23f3a3 --- /dev/null +++ b/fe2-android/docs/images/architecture_diagram.drawio.svg @@ -0,0 +1,4 @@ + + + +
    Network
    Network
    Backend(s)
    Backend(s)
    Backend(s)
    Backend(s)
    Backend(s)
    Backend(s)
    Global Network Manager
    Global Network Manag...
    Creates / Destroy
    Creates / Destroy
    LAO Network Manager
    JsonΒ RPC Layer
    JsonΒ RPC Layer
    Request System
    Request System
    Message Handler
    Message Handler
    Data Handler
    Data Handler
    Data Handler
    Data Handler
    Data Handlers
    Data Handlers
    Data Registry
    Data Registry
    Broadcast
    Broadcast
    User Interface
    -
    View Models
    User Interface...
    Connection(s)
    Scarlet
    Scarlet
    Http Client
    Http Client
    WebSoket
    WebSoket
    Json Parser
    Json Parser

    Remote

    Remote
    Repositories
    Repositories
    State
    State
    Text is not SVG - cannot display
    \ No newline at end of file From 527747e4dadaa4f21db1759b4b0aa4f4d3a1b220 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Tue, 14 Feb 2023 14:27:30 +0100 Subject: [PATCH 106/121] Use str.replace() instead of 2 strings --- fe1-web/src/features/evoting/screens/CreateElection.tsx | 4 +--- fe1-web/src/resources/strings.ts | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/fe1-web/src/features/evoting/screens/CreateElection.tsx b/fe1-web/src/features/evoting/screens/CreateElection.tsx index 118f81f618..81398bf6cb 100644 --- a/fe1-web/src/features/evoting/screens/CreateElection.tsx +++ b/fe1-web/src/features/evoting/screens/CreateElection.tsx @@ -309,9 +309,7 @@ const CreateElection = () => { )} {questions.some(isQuestionInvalid) && ( - {STRINGS.election_create_invalid_questions_1 + - MIN_BALLOT_OPTIONS + - STRINGS.election_create_invalid_questions_2} + {STRINGS.election_create_invalid_questions.replace('{}', MIN_BALLOT_OPTIONS.toString())} )} {!haveQuestionsSameTitle(questions) && ( diff --git a/fe1-web/src/resources/strings.ts b/fe1-web/src/resources/strings.ts index dad2e1b8c8..f98e9bfea4 100644 --- a/fe1-web/src/resources/strings.ts +++ b/fe1-web/src/resources/strings.ts @@ -235,9 +235,8 @@ namespace STRINGS { export const election_create_question = 'Question'; export const election_create_question_placeholder = 'What is your favorite color?'; export const election_create_option_placeholder = 'Blue'; - export const election_create_invalid_questions_1 = - 'All questions must have a title and at least '; - export const election_create_invalid_questions_2 = ' different ballot options.'; + export const election_create_invalid_questions = + 'All questions must have a title and at least {} different ballot options.'; export const election_create_same_questions = 'Two or more questions are the same.'; export const election_create_add_question = 'Add Question'; From 9489f60050f079e0efb11055c0a239d9f802c050 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Tue, 14 Feb 2023 16:30:08 +0100 Subject: [PATCH 107/121] Update README --- fe1-web/docs/README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index 0e365ddd2a..0cbf80afec 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -47,6 +47,12 @@ Here's the annotated directory tree: β”‚ β”‚ β”œβ”€β”€ components # library of simple, reusable UI component β”‚ β”‚ +β”‚ β”œβ”€β”€ contexts # feature context type definition +β”‚ β”‚ +β”‚ β”œβ”€β”€ functions # module containing functions that are useful in the whole application +β”‚ β”‚ +β”‚ β”œβ”€β”€ hooks # contains a wrapper for a useful react hook +β”‚ β”‚ β”‚ β”œβ”€β”€ keypair # module dealing with the storage of a global, unique keypair β”‚ β”‚ β”‚ β”œβ”€β”€ navigation # module dealing with the top-level React navigation @@ -54,6 +60,7 @@ Here's the annotated directory tree: β”‚ β”œβ”€β”€ network # module to network with the backend β”‚ β”‚ β”œβ”€β”€ ingestion # implementation and configuration of the processing of incoming messages β”‚ β”‚ β”œβ”€β”€ jsonrpc # network & protocol objects +β”‚ β”‚ β”œβ”€β”€ strategies # sending strategies when there are multiple server β”‚ β”‚ └── validation # protocol validation utilities β”‚ β”‚ β”‚ β”œβ”€β”€ objects # module containing the core business objects @@ -62,14 +69,23 @@ Here's the annotated directory tree: β”‚ β”‚ β”‚ β”œβ”€β”€ redux # module dealing with the global configuration of the application state (Redux-based) β”‚ β”‚ -β”‚ └── styles # stylesheets +β”‚ β”œβ”€β”€ styles # stylesheets +β”‚ β”‚ +β”‚ └── types # some generic types β”‚ β”œβ”€β”€ features # independent features in the system β”‚ -β”‚ β”œβ”€β”€ connect # feature dealing with LAO/server connection +β”‚ β”œβ”€β”€ digital cash # feature dealing with digital cash +β”‚ β”‚ +β”‚ β”œβ”€β”€ events # feature dealing with events happening in a LAO +β”‚ β”‚ +β”‚ β”œβ”€β”€ evoting # feature dealing with E-Voting and Elections +β”‚ β”‚ +β”‚ β”œβ”€β”€ home # feature dealing with the app's home screen β”‚ β”‚ β”‚ β”œβ”€β”€ lao # feature dealing with the notion of a LAO, showing the typical feature structure β”‚ β”‚ β”œβ”€β”€ components # feature components +β”‚ β”‚ β”œβ”€β”€ errors # feature errors definition β”‚ β”‚ β”œβ”€β”€ functions # feature functions β”‚ β”‚ β”œβ”€β”€ hooks # feature hooks β”‚ β”‚ β”œβ”€β”€ interface # defines the dependencies of the feature and the interface it exposes @@ -80,12 +96,6 @@ Here's the annotated directory tree: β”‚ β”‚ β”œβ”€β”€ screens # UI screens of the feature β”‚ β”‚ └── store # static access to the feature's reducer store (DEPRECATED) β”‚ β”‚ -β”‚ β”œβ”€β”€ events # feature dealing with events happening in a LAO -β”‚ β”‚ -β”‚ β”œβ”€β”€ evoting # feature dealing with E-Voting and Elections -β”‚ β”‚ -β”‚ β”œβ”€β”€ home # feature dealing with the app's home screen -β”‚ β”‚ β”‚ β”œβ”€β”€ meeting # feature dealing with meetings, a type of event β”‚ β”‚ β”‚ β”œβ”€β”€ rollCall # feature dealing with roll calls, a type of event From 4c33f87c7c2302d9e90725850ef8e619c3efae8a Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Tue, 14 Feb 2023 17:29:43 +0100 Subject: [PATCH 108/121] Update fe1-web/src/features/social/reducer/SocialReducer.ts Co-authored-by: Kilian Schneiter <33868847+Xelowak@users.noreply.github.com> --- fe1-web/src/features/social/reducer/SocialReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe1-web/src/features/social/reducer/SocialReducer.ts b/fe1-web/src/features/social/reducer/SocialReducer.ts index 13261d876f..7aa779a6d3 100644 --- a/fe1-web/src/features/social/reducer/SocialReducer.ts +++ b/fe1-web/src/features/social/reducer/SocialReducer.ts @@ -392,7 +392,7 @@ export const makeReactedSelector = (laoId: Hash, chirpId: Hash, user?: PublicKey // add reaction mapping for each code point if the user matches return reactions.reduce>((obj, reaction) => { if (reaction.sender !== serializedPublicKey) { - // skip reactions by other suers + // skip reactions by other users return obj; } From d67f0a59d8dc3066b4ae408b27c46f68783a5268 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Tue, 14 Feb 2023 17:31:27 +0100 Subject: [PATCH 109/121] Update documentation based on @Xelowak's feedback --- fe1-web/src/features/social/network/SocialMessageApi.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fe1-web/src/features/social/network/SocialMessageApi.ts b/fe1-web/src/features/social/network/SocialMessageApi.ts index 99a3359a0a..4b9a3e5bdc 100644 --- a/fe1-web/src/features/social/network/SocialMessageApi.ts +++ b/fe1-web/src/features/social/network/SocialMessageApi.ts @@ -79,11 +79,10 @@ export function requestAddReaction( } /** - * Sends a query to the server to add a new reaction. + * Sends a query to the server to delete an existing reaction. * - * @param reaction_codepoint - The codepoint corresponding to the reaction type - * @param chirp_id - The id of the chirp where the reaction is added - * @param laoId - The id of the Lao in which to add a reaction + * @param reactionId - The id of the reaction that should be deleted + * @param laoId - The id of the lao to which the reaction belongs */ export function requestDeleteReaction(reactionId: Hash, laoId: Hash): Promise { const message = new DeleteReaction({ From b30c4f8cebd47cd7aa864d4bdb6b71cabbc16e5c Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 14 Feb 2023 21:20:31 +0100 Subject: [PATCH 110/121] update documentation read me --- be2-scala/docs/README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/be2-scala/docs/README.md b/be2-scala/docs/README.md index 2e55c21913..d94ea1c8a0 100644 --- a/be2-scala/docs/README.md +++ b/be2-scala/docs/README.md @@ -125,10 +125,10 @@ a source of truth since the validation library checks the messages against it. ```scala register.add( - (ObjectType.LAO, ActionType.CREATE), - SchemaValidator.createSchemaValidator("dataCreateLao.json"), - CreateLao.buildFromJson, - LaoValidator.validateCreateLao, + (ObjectType.LAO, ActionType.CREATE), + SchemaValidator.createSchemaValidator("dataCreateLao.json"), + CreateLao.buildFromJson, + LaoValidator.validateCreateLao, LaoHandler.handleCreateLao ) ``` @@ -206,6 +206,8 @@ Summary of the keys used to retrieve data: - for a message: `channel#message_id` - for ChannelData: `channel` - for LaoData: `root/lao_id#laodata` +- for RollCallData: `root/rollcall/lao_id` +- for ElectionData: `root/private/election_id` We use `/` as a separator for parts of a channel and `#` as a separator for data objects when needed. @@ -303,7 +305,26 @@ final case class DbActorReadLaoDataAck(laoData: LaoData) extends DbActorMessage For the Social Media functionality, each user has their own channel with the identifier `root/lao_id/own_pop_token` and each broadcast containing the message_id of a post will be written to `root/lao_id/posts`. +For the Election functionality, we need to have a key pair stored safely somewhere so that we can encrypt/decrypt messages. That is why we use a `ELectionData` object to store the key pairs for the corresponding election. +The path root is `root/private/election_id` as stated above. +The key pair can be stored and retrieved by the following functions. +```scala +final case class CreateElectionData(id: Hash, keyPair: KeyPair) extends Event +final case class ReadElectionData(electionId: Hash) extends Event + +final case class DbActorReadElectionDataAck(electionData: ElectionData) extends DbActorMessage +``` + +The RollCallData is an object that stores the id and state of the previous rollcall action (`CREATE`, `OPEN`, `REOPEN`, `CLOSE`). It ensures that we cannot open a closed rollcall or close a non-opened rollcall. +The stored parameters can be modified or retrieved by the following functions. + +```scala +final case class ReadRollCallData(laoId: Hash) extends Event +final case class WriteRollCallData(laoId: Hash, message: Message) extends Event + +final case class DbActorReadRollCallDataAck(rollcallData: RollCallData) extends DbActorMessage +``` :information_source: the database may easily be reset/purged by deleting the `database` folder entirely. You may add the `-Dclean` flag at compilation for automatic database purge From bd63c2710cc0201058e3e509db13d9cea7653d44 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 14 Feb 2023 21:21:15 +0100 Subject: [PATCH 111/121] update external libraries doc --- be2-scala/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/be2-scala/README.md b/be2-scala/README.md index bb66683d16..7394f3ae25 100644 --- a/be2-scala/README.md +++ b/be2-scala/README.md @@ -63,6 +63,7 @@ The project relies on several sbt dependencies (external libraries) : - database : [**leveldb**](https://github.com/codeborui/leveldb-scala) which relies on both [snappy](https://search.maven.org/artifact/org.xerial.snappy/snappy-java/1.1.7.3/jar) (for compression/decompression) and [akka-persistence](https://doc.akka.io/docs/akka/current/persistence.html) - Json parser : [**spray-json**](https://github.com/spray/spray-json) for Json encoding/decoding - encryption : [**tink**](https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md) to verify signatures +- encryption : [**kyber**](https://github.com/dedis/kyber) to encrypt and decrypt messages of an election - testing : [**scalatest**](https://www.scalatest.org/) for unit tests - Json schema validator : [**networknt**](https://github.com/networknt/json-schema-validator) for Json schema validation From 8a3316311c3a305763ce7ddcfdebd7b3e97bb76d Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 14 Feb 2023 21:32:30 +0100 Subject: [PATCH 112/121] standardize camelCase --- .../graph/handlers/SocialMediaHandler.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index aa1a05f960..5820b25f29 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -38,7 +38,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { private final val unknownAnswerDatabase: String = "Database actor returned an unknown answer" - private def generateSocialChannel(lao_id: Hash): Channel = Channel(Channel.ROOT_CHANNEL_PREFIX + lao_id + Channel.SOCIAL_MEDIA_CHIRPS_PREFIX) + private def generateSocialChannel(laoId: Hash): Channel = Channel(Channel.ROOT_CHANNEL_PREFIX + laoId + Channel.SOCIAL_MEDIA_CHIRPS_PREFIX) def handleAddChirp(rpcMessage: JsonRpcRequest): GraphMessage = { val ask = @@ -50,9 +50,9 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => - val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) + val (chirpId, channelChirp, data, broadcastChannel) = parametersToBroadcast[AddChirp](rpcMessage) // create and propagate the notifyAddChirp message - val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirp_id, channelChirp, data.timestamp) + val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirpId, channelChirp, data.timestamp) Await.result( dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), duration @@ -82,9 +82,9 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { Await.ready(ask, duration).value match { case Some(Success(_)) => - val (chirp_id, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) + val (chirpId, channelChirp, data, broadcastChannel) = parametersToBroadcast[DeleteChirp](rpcMessage) // create and propagate the notifyDeleteChirp message - val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirp_id, channelChirp, data.timestamp) + val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirpId, channelChirp, data.timestamp) Await.result( dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), duration @@ -132,12 +132,12 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { */ private def parametersToBroadcast[T](rpcMessage: JsonRpcRequest): (Hash, Channel, T, Channel) = { val channelChirp: Channel = rpcMessage.getParamsChannel - val lao_id: Hash = channelChirp.decodeChannelLaoId.get - val broadcastChannel: Channel = generateSocialChannel(lao_id) + val laoId: Hash = channelChirp.decodeChannelLaoId.get + val broadcastChannel: Channel = generateSocialChannel(laoId) val params: Message = rpcMessage.getParamsMessage.get - val chirp_id: Hash = params.message_id + val chirpId: Hash = params.message_id val data: T = params.decodedData.get.asInstanceOf[T] - (chirp_id, channelChirp, data, broadcastChannel) + (chirpId, channelChirp, data, broadcastChannel) } } From d67a7f5a97a6d7ea4cd35fb617c15cff85df65fb Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Tue, 14 Feb 2023 21:43:24 +0100 Subject: [PATCH 113/121] change parameter type of dbBroadcast --- .../epfl/pop/pubsub/graph/handlers/ElectionHandler.scala | 4 ++-- .../ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala | 4 ++-- .../epfl/pop/pubsub/graph/handlers/MessageHandler.scala | 9 +++++---- .../pop/pubsub/graph/handlers/SocialMediaHandler.scala | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala index 1fe986403c..ce54f91929 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/ElectionHandler.scala @@ -66,7 +66,7 @@ class ElectionHandler(dbRef: => AskableActorRef) extends MessageHandler { case SECRET_BALLOT => val keyElection: KeyElection = KeyElection(electionId, keyPair.publicKey) Await.result( - dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, KeyElectionFormat.write(keyElection).toString, electionChannel), + dbBroadcast(rpcMessage, rpcMessage.getParamsChannel, KeyElectionFormat.write(keyElection), electionChannel), duration ) } @@ -115,7 +115,7 @@ class ElectionHandler(dbRef: => AskableActorRef) extends MessageHandler { // data to be broadcast resultElection: ResultElection = ResultElection(electionQuestionResults, witnessSignatures) // create & propagate the resultMessage - _ <- dbBroadcast(rpcMessage, electionChannel, resultElectionFormat.write(resultElection).toString, electionChannel) + _ <- dbBroadcast(rpcMessage, electionChannel, resultElectionFormat.write(resultElection), electionChannel) } yield () Await.ready(combined, duration).value match { case Some(Success(_)) => Left(rpcMessage) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala index ee175d1b72..576dd68c4f 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/LaoHandler.scala @@ -7,7 +7,7 @@ import ch.epfl.pop.model.network.JsonRpcRequest import ch.epfl.pop.model.network.method.message.Message import ch.epfl.pop.model.network.method.message.data.ObjectType import ch.epfl.pop.model.network.method.message.data.lao.{CreateLao, GreetLao, StateLao} -import ch.epfl.pop.model.objects.{Base64Data, Channel, DbActorNAckException, Hash} +import ch.epfl.pop.model.objects.{Channel, DbActorNAckException, Hash} import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor @@ -53,7 +53,7 @@ case object LaoHandler extends MessageHandler { _ <- dbActor ? DbActor.WriteLaoData(laoChannel, message, address) // after creating the lao, we need to send a lao#greet message to the frontend greet: GreetLao = GreetLao(data.id, params.get.sender, address.get, List.empty) - _ <- dbBroadcast(rpcMessage, laoChannel, GreetLaoFormat.write(greet).toString(), laoChannel) + _ <- dbBroadcast(rpcMessage, laoChannel, GreetLaoFormat.write(greet), laoChannel) } yield () Await.ready(combined, duration).value.get match { diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala index 701c4e6642..88f36cfded 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/MessageHandler.scala @@ -8,10 +8,11 @@ import ch.epfl.pop.pubsub.AskPatternConstants import ch.epfl.pop.pubsub.graph.{ErrorCodes, GraphMessage, PipelineError} import ch.epfl.pop.storage.DbActor import ch.epfl.pop.storage.DbActor.DbActorReadLaoDataAck +import spray.json.JsValue import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Future} -import scala.util.{Success} +import scala.concurrent.Future +import scala.util.Success trait MessageHandler extends AskPatternConstants { @@ -91,7 +92,7 @@ trait MessageHandler extends AskPatternConstants { * @return * the database answer wrapped in a [[scala.concurrent.Future]] */ - def dbBroadcast(rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: String, broadcastChannel: Channel): Future[GraphMessage] = { + def dbBroadcast(rpcMessage: JsonRpcRequest, channel: Channel, broadcastData: JsValue, broadcastChannel: Channel): Future[GraphMessage] = { val m: Message = rpcMessage.getParamsMessage.getOrElse( return Future { Right(PipelineError(ErrorCodes.SERVER_ERROR.id, s"dbAskWritePropagate failed : retrieve empty rpcRequest message", rpcMessage.id)) @@ -100,7 +101,7 @@ trait MessageHandler extends AskPatternConstants { val combined = for { DbActorReadLaoDataAck(laoData) <- dbActor ? DbActor.ReadLaoData(channel) - encodedData: Base64Data = Base64Data.encode(broadcastData) + encodedData: Base64Data = Base64Data.encode(broadcastData.toString) broadcastSignature: Signature = laoData.privateKey.signData(encodedData) broadcastId: Hash = Hash.fromStrings(encodedData.toString, broadcastSignature.toString) broadcastMessage: Message = Message(encodedData, laoData.publicKey, broadcastSignature, broadcastId, List.empty) diff --git a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala index 5820b25f29..66c295f72c 100644 --- a/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala +++ b/be2-scala/src/main/scala/ch/epfl/pop/pubsub/graph/handlers/SocialMediaHandler.scala @@ -54,7 +54,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { // create and propagate the notifyAddChirp message val notifyAddChirp: NotifyAddChirp = NotifyAddChirp(chirpId, channelChirp, data.timestamp) Await.result( - dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson.toString, broadcastChannel), + dbBroadcast(rpcMessage, channelChirp, notifyAddChirp.toJson, broadcastChannel), duration ) @@ -86,7 +86,7 @@ class SocialMediaHandler(dbRef: => AskableActorRef) extends MessageHandler { // create and propagate the notifyDeleteChirp message val notifyDeleteChirp: NotifyDeleteChirp = NotifyDeleteChirp(chirpId, channelChirp, data.timestamp) Await.result( - dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson.toString, broadcastChannel), + dbBroadcast(rpcMessage, channelChirp, notifyDeleteChirp.toJson, broadcastChannel), duration ) From 702b8f14b7b85c426dc86afedca61b74d6e38293 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter <33868847+Xelowak@users.noreply.github.com> Date: Wed, 15 Feb 2023 14:51:48 +0100 Subject: [PATCH 114/121] Update fe1-web/docs/README.md Co-authored-by: pierluca --- fe1-web/docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index 0cbf80afec..eaeb82d90a 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -51,7 +51,7 @@ Here's the annotated directory tree: β”‚ β”‚ β”‚ β”œβ”€β”€ functions # module containing functions that are useful in the whole application β”‚ β”‚ -β”‚ β”œβ”€β”€ hooks # contains a wrapper for a useful react hook +β”‚ β”œβ”€β”€ hooks # contains utility react hooks β”‚ β”‚ β”‚ β”œβ”€β”€ keypair # module dealing with the storage of a global, unique keypair β”‚ β”‚ From cb6b70775fbd56f23022ce3b705b0c4c5f858dfa Mon Sep 17 00:00:00 2001 From: Kilian Schneiter <33868847+Xelowak@users.noreply.github.com> Date: Wed, 15 Feb 2023 14:52:02 +0100 Subject: [PATCH 115/121] Update fe1-web/docs/README.md Co-authored-by: Nico Hauser --- fe1-web/docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index eaeb82d90a..501bddae9b 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -60,7 +60,7 @@ Here's the annotated directory tree: β”‚ β”œβ”€β”€ network # module to network with the backend β”‚ β”‚ β”œβ”€β”€ ingestion # implementation and configuration of the processing of incoming messages β”‚ β”‚ β”œβ”€β”€ jsonrpc # network & protocol objects -β”‚ β”‚ β”œβ”€β”€ strategies # sending strategies when there are multiple server +β”‚ β”‚ β”œβ”€β”€ strategies # sending strategies when there are multiple servers β”‚ β”‚ └── validation # protocol validation utilities β”‚ β”‚ β”‚ β”œβ”€β”€ objects # module containing the core business objects From 5cd45fb49ebdc685a44ecc9fb418cecea924a101 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Wed, 15 Feb 2023 14:55:28 +0100 Subject: [PATCH 116/121] Move lao feature to the top --- fe1-web/docs/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index 501bddae9b..2724069a3c 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -75,14 +75,6 @@ Here's the annotated directory tree: β”‚ β”œβ”€β”€ features # independent features in the system β”‚ -β”‚ β”œβ”€β”€ digital cash # feature dealing with digital cash -β”‚ β”‚ -β”‚ β”œβ”€β”€ events # feature dealing with events happening in a LAO -β”‚ β”‚ -β”‚ β”œβ”€β”€ evoting # feature dealing with E-Voting and Elections -β”‚ β”‚ -β”‚ β”œβ”€β”€ home # feature dealing with the app's home screen -β”‚ β”‚ β”‚ β”œβ”€β”€ lao # feature dealing with the notion of a LAO, showing the typical feature structure β”‚ β”‚ β”œβ”€β”€ components # feature components β”‚ β”‚ β”œβ”€β”€ errors # feature errors definition @@ -96,6 +88,14 @@ Here's the annotated directory tree: β”‚ β”‚ β”œβ”€β”€ screens # UI screens of the feature β”‚ β”‚ └── store # static access to the feature's reducer store (DEPRECATED) β”‚ β”‚ +β”‚ β”œβ”€β”€ digital cash # feature dealing with digital cash +β”‚ β”‚ +β”‚ β”œβ”€β”€ events # feature dealing with events happening in a LAO +β”‚ β”‚ +β”‚ β”œβ”€β”€ evoting # feature dealing with E-Voting and Elections +β”‚ β”‚ +β”‚ β”œβ”€β”€ home # feature dealing with the app's home screen +β”‚ β”‚ β”‚ β”œβ”€β”€ meeting # feature dealing with meetings, a type of event β”‚ β”‚ β”‚ β”œβ”€β”€ rollCall # feature dealing with roll calls, a type of event From 0c75163212f2d426d743cf6f372050da572146dd Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Wed, 15 Feb 2023 15:05:07 +0100 Subject: [PATCH 117/121] Add notifications --- fe1-web/docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index 2724069a3c..8f9d2fd43f 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -98,6 +98,8 @@ Here's the annotated directory tree: β”‚ β”‚ β”‚ β”œβ”€β”€ meeting # feature dealing with meetings, a type of event β”‚ β”‚ +β”‚ β”œβ”€β”€ notification # feature dealing with notifications +β”‚ β”‚ β”‚ β”œβ”€β”€ rollCall # feature dealing with roll calls, a type of event β”‚ β”‚ β”‚ β”œβ”€β”€ social # feature dealing with social media functionality From d5761555b97b8047c39d294eeda136db013fb4a0 Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Wed, 15 Feb 2023 15:25:44 +0100 Subject: [PATCH 118/121] Update social media dependencies --- fe1-web/src/features/social/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fe1-web/src/features/social/README.md b/fe1-web/src/features/social/README.md index 53fc499441..0da1347fb5 100644 --- a/fe1-web/src/features/social/README.md +++ b/fe1-web/src/features/social/README.md @@ -5,6 +5,8 @@ participants to "chirp", react to chirps and follow each other. ## Dependencies -**Note**: This feature has not been separated from the other features yet and -thus the exact dependencies are a bit unclear. Running `npm run depcruise` -suggests that it depends on the `rollCall`, `lao` and `wallet` features. +- Basic LAO functionality provided by the `lao` feature +- LAO Events support provided by the `events` feature +- Wallet functions to generate PoP tokens + +See `interface/Configuration.ts` for more details. From 69f3ef0c497003b0ab346a53b480ced73fb80852 Mon Sep 17 00:00:00 2001 From: Ajkunas Date: Wed, 15 Feb 2023 16:46:23 +0100 Subject: [PATCH 119/121] correct typo --- be2-scala/docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be2-scala/docs/README.md b/be2-scala/docs/README.md index d94ea1c8a0..e06344ff9b 100644 --- a/be2-scala/docs/README.md +++ b/be2-scala/docs/README.md @@ -305,7 +305,7 @@ final case class DbActorReadLaoDataAck(laoData: LaoData) extends DbActorMessage For the Social Media functionality, each user has their own channel with the identifier `root/lao_id/own_pop_token` and each broadcast containing the message_id of a post will be written to `root/lao_id/posts`. -For the Election functionality, we need to have a key pair stored safely somewhere so that we can encrypt/decrypt messages. That is why we use a `ELectionData` object to store the key pairs for the corresponding election. +For the Election functionality, we need to have a key pair stored safely somewhere so that we can encrypt/decrypt messages. That is why we use a `ElectionData` object to store the key pairs for the corresponding election. The path root is `root/private/election_id` as stated above. The key pair can be stored and retrieved by the following functions. @@ -316,7 +316,7 @@ final case class ReadElectionData(electionId: Hash) extends Event final case class DbActorReadElectionDataAck(electionData: ElectionData) extends DbActorMessage ``` -The RollCallData is an object that stores the id and state of the previous rollcall action (`CREATE`, `OPEN`, `REOPEN`, `CLOSE`). It ensures that we cannot open a closed rollcall or close a non-opened rollcall. +The RollCallData is an object that stores the id and state of the latest rollcall action (`CREATE`, `OPEN`, `REOPEN`, or `CLOSE`). It ensures that we cannot open a closed rollcall or close a non-opened rollcall. The stored parameters can be modified or retrieved by the following functions. ```scala From ae1e1787c01d501e348963a927658a018287f7fe Mon Sep 17 00:00:00 2001 From: Kilian Schneiter Date: Wed, 15 Feb 2023 17:16:38 +0100 Subject: [PATCH 120/121] Add '-' in digital-cash --- fe1-web/docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe1-web/docs/README.md b/fe1-web/docs/README.md index 8f9d2fd43f..acce5ecabb 100644 --- a/fe1-web/docs/README.md +++ b/fe1-web/docs/README.md @@ -88,7 +88,7 @@ Here's the annotated directory tree: β”‚ β”‚ β”œβ”€β”€ screens # UI screens of the feature β”‚ β”‚ └── store # static access to the feature's reducer store (DEPRECATED) β”‚ β”‚ -β”‚ β”œβ”€β”€ digital cash # feature dealing with digital cash +β”‚ β”œβ”€β”€ digital-cash # feature dealing with digital cash β”‚ β”‚ β”‚ β”œβ”€β”€ events # feature dealing with events happening in a LAO β”‚ β”‚ From edd6bb48193674bb6810e65f13d75301a4497347 Mon Sep 17 00:00:00 2001 From: Nico Hauser Date: Wed, 15 Feb 2023 19:16:26 +0100 Subject: [PATCH 121/121] Remove outdated text --- .../features/social/screens/SocialHome.tsx | 6 +----- .../__snapshots__/SocialHome.test.tsx.snap | 21 ------------------- fe1-web/src/resources/strings.ts | 2 -- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/fe1-web/src/features/social/screens/SocialHome.tsx b/fe1-web/src/features/social/screens/SocialHome.tsx index fd0f387b9f..12c48db90d 100644 --- a/fe1-web/src/features/social/screens/SocialHome.tsx +++ b/fe1-web/src/features/social/screens/SocialHome.tsx @@ -38,11 +38,7 @@ const SocialHome = () => { {STRINGS.social_media_create_chirps_yet} - {currentUserPopTokenPublicKey ? ( - - {STRINGS.social_media_howto_create_chirps} - - ) : ( + {!currentUserPopTokenPublicKey && ( {STRINGS.social_media_create_chirp_no_pop_token} diff --git a/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialHome.test.tsx.snap b/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialHome.test.tsx.snap index 7e36433cd4..96aeaceb09 100644 --- a/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialHome.test.tsx.snap +++ b/fe1-web/src/features/social/screens/__tests__/__snapshots__/SocialHome.test.tsx.snap @@ -42,27 +42,6 @@ exports[`SocialHome renders correctly 1`] = ` > So far nobody has published a chirp and you could be the first one to do so! - - The button for doing so is located in the navigation bar all the way to the right. Be creative ✨ -